作为程序员,你们中有多少人对编程安全或安全编程有很好的理解?这与应用安全或网络安全是不一样的。我必须承认;在我职业生涯的早期,我对这些了解不多,尤其是我不是计算机科学背景的人。但回过头来看,我认为编程安全是每个程序员都应该知道的,而且应该在初级阶段就被教授。
什么是安全编程,或者更准确地说,安全对于一种编程语言意味着什么?或者说,不安全意味着什么?让我们先设定一下背景。
如果你想通过观看视频来了解情况,请查看我在FOSDEM'22上就同一主题所做的演讲视频,下面是OktaDev的YouTube频道。
编程安全
编程安全=内存安全+类型安全+线程安全
当我们谈论编程中的 "安全 "时,我们指的是三种不同事物的某种组合:内存安全、类型安全和线程安全。如果你把null安全算作与内存安全不同的东西,那就有四个,但我们今天把这两个放在一起。
内存安全
在内存安全的语言中,当你访问一个变量或数组中的一个项目时,你可以确定你确实在访问你想要访问的或允许访问的东西。换句话说,无论你在程序中做什么,你都不会错误地读或写到另一个变量或指针的内存中。
那么,为什么这是个大问题呢?所有主要的编程语言不都是这样保证的吗?
是的,在不同的程度上。但有些语言默认是不安全的--例如,C和C++。在C或C++中,你可以错误地访问另一个变量的内存,或者你可以释放一个指针两次;这被称为无双错误。有时,程序在释放了一个指针后还继续使用它,这被称为释放后使用(UAF)错误或悬空的指针错误。这种行为被归类为未定义的行为;它们是不可预测的,并且会导致安全漏洞,而不仅仅是使程序崩溃。在这些情况下,程序崩溃是一件好事,因为它不会造成安全漏洞。
我把它称为我的十亿美元的错误。这是在1965年发明的空引用。
- Tony Hoare
然后还有null安全,这与内存安全有点关系。我来自Java/JavaScript背景,我们已经习惯了null的概念。空值是臭名昭著的,是编程中最糟糕的发明。垃圾收集语言需要 "空 "的概念,以便指针在未使用时可以被释放。但是这个概念也导致了一些问题和痛苦,比如空指针的异常。从技术上讲,这与内存安全有关,但大多数内存安全语言仍然让你使用null作为一个值,导致空指针错误。
类型安全
在类型安全的语言中,当你访问一个变量时,你会根据它的存储方式将其作为正确的数据类型来访问。这使我们有信心对数据进行处理,而无需在运行时手动检查数据类型。内存安全是语言类型安全的必要条件。
线程安全
在线程安全的语言中,你可以同时从多个线程访问或修改相同的内存,而不用担心数据竞赛。这通常是通过消息传递技术、互斥锁(mutexes)和线程同步实现的。线程安全是最佳内存和类型安全的要求,所以一般来说,内存和类型安全的语言往往也是线程安全的。
这有什么关系呢?
好吧!为什么这很重要,我们为什么要关心?让我们先看看一些统计数字来了解一下。
内存安全问题
内存安全问题是我们遇到的大多数安全CVE(常见漏洞和暴露)的原因。未定义行为可以被黑客滥用,以控制程序或泄露特权信息。如果你试图在内存安全的语言中访问一个越界的数组元素,你只会以恐慌或错误的方式让程序崩溃,这是可以预测的行为。
这就是为什么C/C++系统中与内存有关的错误常常导致CVE和紧急补丁的出现。在C/C++中还有其他一些内存不安全的行为,比如从已经被弹出的堆栈帧中访问指针,一个已经被取消分配的内存,迭代器无效,等等。内存安全语言,即使不是最安全的语言,也能防止这种安全问题。
如果我们看一下统计数据,我们可以看到:
- 在微软的所有CVE中,大约70%是内存安全问题。
- 三分之二的Linux内核漏洞来自内存安全问题。
- 苹果公司的一项研究发现,iOS和macOS中60-70%的漏洞是内存安全漏洞。
- 谷歌估计,90%的安卓系统漏洞是内存安全问题。
- 70%的Chrome安全漏洞都是内存安全问题。
- 对被发现在野外被利用的0日的分析发现,超过80%的被利用的-漏洞是内存安全问题。
- 有史以来最流行的一些安全问题是内存安全问题。
这是一大块CVE,当然,大部分来自C/C++系统也就不足为奇了 🤷
又是一天,又是一个C/C++内存安全漏洞CVE 🤷♂️🤦♂️#areWeMemorySafeYet#replaceWithRust#dirtyPipe #cpphttps://t.co/ENqeD3gA1u
- Deepu K Sasidharan ( ദീപു, தீபு, दीपू ) ( @deepu105)2022年3月8日
想象一下一个没有内存安全问题的世界。想象一下,可以节省多少开发者的时间,节省多少金钱,节省多少资源。有时我想知道为什么我们还在使用C/C++。为什么我们要相信人类,不顾所有可用的证据,手动处理内存?这还没有考虑其他非CVE的内存问题,如内存泄漏、内存效率等等。
线程安全问题
虽然不像内存安全那样臭名昭著,但线程安全也是令开发者头疼的主要原因,并可能导致安全问题。
线程安全问题会导致两种类型的漏洞:
- 一个线程覆盖另一个线程的信息而造成的信息损失
- 指针损坏,允许权限升级或远程执行
- 多线程信息交错导致的完整性损失
- 这类攻击中最著名的是TOCTOU(检查时间到使用时间)攻击,这是检查条件(如安全凭证)和使用结果之间的一个竞赛条件。
信息丢失和完整性丢失都可以被利用并导致安全问题。虽然与线程安全相关的攻击比内存安全的攻击更难、更不常见,但它们仍然是可能的。
类型安全问题
虽然没有内存和线程安全那么关键,但缺乏类型安全也会导致安全问题,而类型安全对于确保内存安全很重要。
在没有类型安全的语言中,低级别的漏洞是可能的,因为攻击者可以操纵数据结构,改变数据类型,从而获得特权信息。虽然这种类型的漏洞相当罕见,但也不是没有发生过。
为什么是Rust?
现在我们明白了编程安全的重要性,让我们看看为什么Rust是最安全的语言之一,以及它是如何避免我们通常在C/C++等语言中遇到的大多数安全问题的。
对于那些不熟悉的人来说,Rust是一种高级多范式语言。它是函数式和命令式编程的理想选择。它有非常现代的,而且在我看来,是最好的编程语言工具。虽然它最初是作为一种系统编程语言设计的,但它的优势和灵活性使它作为一种通用语言适用于各种使用情况。
"Rust在其文档中抛出了一些流行语,但它们并不只是营销上的流行语;它们实际上是带着十足的诚意的,而且它们非常重要。"
Rust的安全保证
安全保证是Rust最重要的方面之一;Rust在设计上是内存安全的、空值安全的、类型安全的和线程安全的。
如果编译器检测到不安全的代码,它将默认拒绝编译该代码。你必须使用unsafe 关键字来打破这些保证。因此,即使在你不得不写不安全代码的情况下,你也会明确地表达出来,因此问题很容易被追踪到特定的代码块。
Rust中的内存安全
Rust使用其创新的所有权机制和编译器中的借贷检查器在编译时确保内存安全。编译器不允许内存不安全的代码,除非它在不安全块或函数中被明确标记为不安全。这种静态的编译时分析消除了许多类型的内存错误,再加上一些额外的运行时检查,Rust保证了内存安全。 在语言层面上没有null的概念。相反,Rust提供了Option 枚举,它可以用来标记一个值的存在或不存在。这使得产生的代码是安全的,而且更容易处理,在Rust中你永远不会遇到空指针的异常。
所有权和借用机制使Rust成为内存效率最高的语言之一,同时避免了手动内存管理和垃圾收集的陷阱。它的内存效率和速度可与C/C++相媲美,而内存安全性则优于Java和Go等垃圾收集语言。
我在个人博客中写过关于不同语言的内存管理的详细文章,如果你有兴趣了解更多关于Java、Rust、JavaScript和Go的内存管理,请查看这些文章。
Rust中的类型安全
Rust是静态类型化的,它通过严格的编译时类型检查和保证内存安全来保证类型安全。这并不特别,因为大多数现代语言都是静态类型的。Rust也允许一定程度的动态类型化,在需要时使用dyn 关键字和Any 类型。但是强大的类型推理和编译器即使在这些情况下也能保证类型安全。
Rust中的线程安全
Rust使用类似于内存安全的概念来保证线程安全,并提供标准的库功能,如通道、互斥和ARC(原子参考计数)指针。在安全的Rust中,你可以在任何时候对一个值有一个可变的引用,或者对它有无限的只读引用。所有权机制使得不可能从共享状态中引起意外的数据竞赛。这使得我们有信心专注于代码,让编译器担心线程之间的共享数据。
其他Rust特性
我在我的博客上写了一篇关于我对Rust的印象的详细文章,其中我解释了Rust的优秀特性,这些特性使它独一无二。下面是对这些特性的简短总结:
- 零成本抽象:Rust提供了真正的零成本抽象,这意味着你可以用任何数量的抽象编写任何风格的代码,而不需要付出任何性能上的代价。很少有语言能提供这一点,这就是为什么Rust如此之快。无论你写的是什么风格的代码,Rust编译器都会生成最好的字节代码。这意味着你可以编写函数式代码,并获得与命令式代码相同的性能。
- 默认情况下是不可变的:Rust中的值默认是不可变的,或只读的。可变性必须要明确声明。这一点,加上通过值或引用传递的能力,使得编写没有副作用的函数式代码变得非常容易。
- 模式匹配:Rust对高级模式匹配有很好的支持。模式匹配在Rust中被广泛用于错误处理和控制流。
- 高级泛型、特质和类型:Rust拥有先进的泛型和特质,支持类型别名和类型推理。虽然泛型在与生命期结合时很容易变得复杂,但它是Rust最强大的功能之一。
- 宏:支持使用宏来进行元编程。Rust同时支持声明性宏和程序性宏。宏可以像注释、属性和函数一样使用。
- 伟大的工具和最好的编译器之一:Rust是我所见过的最好的编译器和最好的工具之一(与JS世界、JVM语言、Go、Python、Ruby、CSharp、PHP、C/C++相比)。它还有优秀的文档,这些文档与工具一起运送,供离线使用。这是多么了不起的事情啊
- 优秀的社区和生态系统:Rust是最有活力和最友好的社区之一。这个生态系统相当年轻,但却是发展最快的系统之一。
通常情况下,一种编程语言会在安全性、速度和高级抽象之间提供一个选择。在最好的情况下,你可以选择其中的两个。例如,在Java/C#/Go中,你以运行时间的开销为代价得到了安全和高级抽象,而C++则以安全为代价给你带来了速度和抽象。但Rust提供了所有这三样东西,并提供了良好的开发者体验作为奖励。我不认为许多其他主流语言可以这样说。
"Rust,而不是Firefox,是Mozilla最大的行业贡献"。
- TechRepublic
这并不意味着没有缺点,而且Rust绝对不是银弹。有一些问题,如陡峭的学习曲线和语言的复杂性。但在我看来,它是最接近银弹的东西。这并不意味着你应该开始使用Rust来处理所有事情。如果一个用例需要速度、并发性、构建系统工具或构建CLI,那么Rust是一个理想的选择。就我个人而言,在任何使用情况下,我都会推荐Rust而不是C/C++,除非你正在为一个Rust不支持的传统平台构建一个工具。