Rust 研学 | Rust 安全内参 2024 Part 1
本系列是对 Rust 安全数据库[1] 中记录的 Rust 及其生态库安全问题的梳理,在梳理过程中,还会通过这些库的源码实现中汲取营养,帮助我们成为更好的开发者。
目录
背景:漏洞分类 ferris-says
非UTF-8字节转换为字符串ferris-says
介绍ferris-says
Unsound 问题详解rustvmm-sys-util
内存越界风险rustvmm
介绍rustvmm-sys-util
漏洞详解tokio-tracing
中的 UAF 非健全问题tokio-tracing
介绍tokio-tracing
Unsound 问题详解扩展阅读:Layer 设计模式与框架生态 小结
背景:漏洞分类
Rust 安全数据库中记录的安全问题分为两大类:安全漏洞(Vulnerability)和非健全(Unsound)问题。
安全漏洞又具体分为以下几类:
代码执行(Code Execution) 内存损坏(Memory Corruption) 权限提升(Privilege Escalation,在操作系统级别或应用程序/库内部) 文件泄露/目录遍历(File Disclosure / Directory Traversal) 网络安全(Web Security,例如 XSS、CSRF) 格式注入(Format Injection),例如 shell 转义、SQL 注入(以及 XSS) 加密失败(Cryptography Failure,例如机密性破坏、完整性破坏、密钥泄漏) 隐蔽通道(例如 Spectre、Meltdown) 代码中的恐慌(panic)标榜为“panic-free”(特别是如果对网络 DoS 攻击有用) 内存暴露(memory-exposure) 线程安全(thread-safety)
此外,RustSec 还会对生态库的健全性[2] 进行跟踪,而不管它们是否是漏洞。 因为 当使用来自安全代码的 crate 可能导致未定义行为[3]时,往往伴随着健全性问题。
健全性(Soundness)是一个类型系统概念(实际上源于逻辑学研究),意味着类型系统在某种意义上是“正确的”,即类型良好的程序实际上具有所需的属性。对于 Rust,这意味着类型良好的程序不会导致未定义的行为[4]。然而,这个承诺只适用于安全代码;对于
unsafe
代码,由程序员来维护这份契约。
对于安全漏洞或非健全问题发生的原因或机制,RustSec 则使用关键字来进行标识。但是 RustSec 的关键字太多了,所以我对关键字也进一步做了一个分类。
分类 | 漏洞机制 | 以2022年为例 | 类型 |
---|---|---|---|
memory safety[5] | alias (多个可变借用) | 2022 年还未见有该漏洞 | 漏洞 |
double free | 2022 年还未见有该漏洞 | ||
use-after-free[6] | incorrect-lifetime | 2022 年 一例 RUSTSEC-2022-0028: Vulnerability in neon[7] | 漏洞 |
wasm | 2022 年 一例 RUSTSEC-2022-0016: Vulnerability in wasmtime[8] | 漏洞 | |
segfault | 2022 年 一例 RUSTSEC-2022-0002: Vulnerability in dashmap[9] | 漏洞 | |
memory-layout | cast | 2022 年 一例 RUSTSEC-2022-0052: Unsoundness in os_socketaddr[10] | Unsound |
cell | 2022 年 一例 RUSTSEC-2020-0164: Unsoundness in cell-project[11] | Unsound | |
ddos[12] | oom | 2022 年两例 RUSTSEC-2022-0055: Vulnerability in axum-core[13] 和 RUSTSEC-2022-0035: Vulnerability in websocket[14] | 漏洞 |
untrust data | 2022 年一例 RUSTSEC-2021-0143: Vulnerability in kamadak-exif[15] | 漏洞 CVSS分 6.5 MEDIUM | |
directory-traversal | Windows | 2022 年 两例: RUSTSEC-2022-0069: Vulnerability in hyper-staticfile[16] 和 RUSTSEC-2022-0043: Vulnerability in tower-http[17] | 漏洞 |
html | xss | 2022 年一例 RUSTSEC-2022-0003: Vulnerability in ammonia[18] | 漏洞 |
integer-overflow | out-of-bounds | 2022 年一例 RUSTSEC-2022-0051: Vulnerability in lz4-sys[19] | 漏洞 CVSS Score 9.8 CRITICAL |
out-of-bounds-read | out-of-bounds-read | 2022 年一例 RUSTSEC-2022-0046: Vulnerability in rocksdb[20] | 漏洞 |
side-channel | timing-attack | 2022 年一例 RUSTSEC-2022-0018: Vulnerability in totp-rs[21] | 漏洞 CVSS Score 4.2 MEDIUM |
stack-overflow | stack-overflow | 2022 年一例 RUSTSEC-2022-0004: Vulnerability in rustc-serialize[22] | 漏洞 |
subtype | variance/cell | 2022 年一例 RUSTSEC-2020-0164: Unsoundness in cell-project[23] | Unsound |
type-confusion | type-confusion | 2022 年一例 RUSTSEC-2020-0165: Unsoundness in mozjpeg[24] | Unsound |
typosquatting | typosquatting (恶意伪造同名 crate) | 2022 年一例 RUSTSEC-2022-0042: Vulnerability in rustdecimal[25] | 漏洞 |
unaligned-read | windows | 20222 年一例 RUSTSEC-2021-0145: Unsoundness in atty[26] | Unsound |
uninitialized-memory | uninitialized-memory | 20222 年两例 RUSTSEC-2022-0067: Unsoundness in lzf[27] 和 RUSTSEC-2018-0022: Vulnerability in temporary[28] | Unsound 和 漏洞 |
unsound | covariant | 2022 年一例 RUSTSEC-2022-0007: Unsoundness in qcell[29] | Unsound |
注:CVSS 即通用漏洞评分系统,参见文后安全术语介绍部分。
CVSS 危害级别划分:
分值范围 [0.1 - 3.9]
–> 低危害性分值范围 [4.0 - 6.9]
–> 中危害性分值范围 [7 - 10]
-> 高危害性
ferris-says
非UTF-8字节转换为字符串
这是来自 Rust Security Advisory Database 编号为 RUSTSEC-2024-0001[30] 的非健全(Unsound)问题。
ferris-says
介绍
ferris-says[31]是一个可以打印 Rust 非官方吉祥物 Ferris 和指定文本的库。使用它你可以在运行时打印出下面图案:
__________________________
< Hello fellow Rustaceans! >
--------------------------
\
\
_~^~^~_
\) / o o \ (/
'_ - _'
/ '-----' \
该库也提供二进制版本,你在 cargo install fsays
安装之后,在终端可以使用 fsays 'Hello fellow Rustaceans!'
命令打印。你还可以定制打印宽度等等。
这虽然是一个非常小型的库,但是它来自于 Rust 官方。所以,安全性需要得到重视。
ferris-says
Unsound 问题详解
该库 unsound 版本的核心函数 say
源码摘要如下:
pub fn say<W>(input: &[u8], max_width: usize, writer: &mut W) -> Result<()>
where
W: Write,
{
// Final output is stored here
let mut write_buffer = SmallVec::<[u8; BUFSIZE]>::new();
// Let textwrap work its magic
let wrapped = fill(unsafe { str::from_utf8_unchecked(input) }, max_width);
// 省略其他代码
}
这里 say
函数是一个安全函数(未标记 unsafe
)。它的参数 input
是一个 &[u8]
,可以理解为就是一个字节序列。这个 input
参数直接被传入到了 unsafe block
中的 str::from_utf8_unchecked
方法。
其实这段代码写的并不符合 Unsafe 安全抽象规范。在 Rust 生态中,早已形成一个约定俗成,就是在使用 unsafe block
的时候,要为其增加注释,比如:
// Let textwrap work its magic
// SAFETY: input 永远都是 UTF-8 编码
let wrapped = fill(unsafe { str::from_utf8_unchecked(input) }, max_width);
上面代码我为其增加了// SAFETY
注释,用于说明该 unsafe block
在这里为什么是安全的。假如我是开发者,我能确定传入的 input
一定会是 UTF-8 编码,所以假设这里是安全的。
但是事实是如此吗? ferris-says
会被用到任何想不到的场景,外部传入的字符串很可能不会是 UTF-8 编码。 所以,这里是不安全的。
遵循 Unsafe 安全抽象规范,对使用 unsafe block
的地方增加 // SAFETY
注释,有助于开发者去深入思考这里的安全边界。
我们发现,这里并不安全,所以需要修改:
pub fn say<W>(input: &[u8], max_width: usize, writer: &mut W) -> Result<()>
where
W: Write,
{
// Final output is stored here
let mut write_buffer = SmallVec::<[u8; BUFSIZE]>::new();
// Let textwrap work its magic
let wrapped = fill(
str::from_utf8(input).map_err(|_| std::io::ErrorKind::InvalidData)?,
max_width,
);
// 省略其他代码
}
这里将 from_utf8_unchecked
换为 from_utf8
,让其检查传入的字节序列是否为 UTF-8 编码,如果不是则报错。这样我们就消除了 Unsound 风险。
该库在 0.3.1
版本中已经修复了这个问题。
rustvmm-sys-util 内存越界风险
这是来自 Rust Security Advisory Database 编号为 RUSTSEC-2024-0002[32] 的安全问题。CVSS 危害等级为 MEDIUM( 5.7
分 )。
rustvmm 介绍
`rust-vmm`[33] 是一个开源项目,它提供了一套虚拟化组件,任何项目都可以使用这些组件快速开发虚拟化解决方案,而无需重新实现常见组件,如 KVM 包装器、virtio 设备和其他 VMM 库。
rust-vmm 项目是一个共同努力、共同拥有的开源项目,目前包括来自阿里巴巴、亚马逊、Cloud Base、Crowdstrike、英特尔、谷歌、Linaro、红帽以及个人贡献者的贡献者。
rust-vmm 包含了很多独立组件,各司其职,其中就包含 `rustvmm-sys-util`[34],该库提供构建虚拟机监视器(VMM)和 hypervisors 的辅助工具和实用程序的模块集合。
hypervisor
是一种将操作系统与硬件抽象分离的方法,以达到 host machine 的硬件能同时运行一个至多个虚拟机作为 guest machine 的目的,这样能够使得这些虚拟机高效地分享主机硬件资源。
在 Rust 生态中,一般以 -sys
作为后缀命名的库或文件目录,通常都意味着这是对对底层资源的 Rust 绑定。vmm-sys-util
也不例外,它封装了x86_64
和 aarch64
平台 和 Linux/Windows
操作系统的安全抽象,用于文件处理、事件文件描述符、ioctls 等基础操作。
rustvmm-sys-util 漏洞详解
在 rustvmm-sys-util
中有一个模块叫 fam
。该模块用于处理 C 语言中的柔性数组成员(Flexible Array Member)[35]。
FAM 也叫 灵活数组成员,是 C 编程语言 C99 标准引入的一个特性。FAM 是一个没有固定维度且大小灵活的数组。这样的数组最好作为结构体的最后一个成员声明,并且它的大小是可变的(可以在运行时更改)。结构体必须除了可变数组成员外,还必须包含至少一个命名成员。用
sizeof
计算结构体大小时,柔性数组成员是不计入结果的。整个结构体的内存是连续的。
在 KVM API中有许多这种类型的结构:包含一个或多个固定大小的字段(Header),后面跟随一个可变大小的数据区域。在这种结构中,Header 通常包含有关数据区域(如其大小、类型或状态)的元数据信息。所以,在如 KVM API 这样的系统编程接口中,FAM 通常被用于处理这种类型的数据结构。
使用 FAM 的一大缺点,就是内存管理的复杂性增加,以及可能存在内存溢出风险。Rust 不直接支持 FAM。
所以,在 rustvmm-sys-util
中这样来实现 FAM :