Rust生态安全漏洞总结系列 | Part 3

作者: 张汉东

本系列主要是分析RustSecurity 安全数据库库中记录的Rust生态社区中发现的安全问题,从中总结一些教训,学习Rust安全编程的经验。

本期分析了下面十一个安全问题:

看是否能给我们一些启示。

RUSTSEC-2021-0110: Vulnerability in wasmtime

在 Wasmtime 中发现多个代码缺陷。包括 UAF(use-after-free)、越界读写等。

漏洞描述:

  • 漏洞类型:Vulnerability
  • 漏洞分类:memory-corruption/ memory-exposure
  • CVE 编号: CVE-2021-39216CVE-2021-39219CVE-2021-39218
  • 详细:https://rustsec.org/advisories/RUSTSEC-2021-0110.html
  • 补丁:>=0.30.0
  • 关键字:use-after-free / out-of-bounds read /out-of-bounds write / Wasm/ garbage collection

漏洞分析

背景: externref 是 WebAssembly 引用类型(Reference Types)中引入的概念,用于表示 Host 引用。

Use after free passing externrefs to Wasm in Wasmtime

当从 Host 传递给 Guest externrefs 时会引发 UAF 。满足下列条件之一可触发此 Bug :

  1. 同时明确地从Host传递多个 externrefswasm 实例
  2. 通过将多个 externrefs 作为参数从 Host 代码传递给 wasm函数
  3. 从Host定义的多值返回函数中返回多个 externrefswasm

如果 WasmtimeVMExternRefActivationsTable在传入第一个externref后容量被填满,那么传入第二个externref可能会触发垃圾回收。然而,在把控制权传给Wasm之前,第一个externref是没有根(root)的,因此,如果没有其他东西持有对它的引用或以其他方式保持它的live,就会被GC回收。然后,当控制权在垃圾收集后被传递给Wasm时,Wasm可以使用第一个externref,但这时它已经被释放了。

Out-of-bounds read/write and invalid free with externrefs and GC safepoints in Wasmtime

Wasmtime运行使用externrefsWasm时,存在一个无效释放和越界读写的错误。

要触发这个错误,Wasmtime需要运行使用externrefsWasm,Host 创建非空的externrefsWasmtime执行一个垃圾收集(GC),并且堆栈上必须有一个Wasm帧,它处于GC Safepoint(安全点就是指代码运行到这个地方,它的状态是确定的, GC就可以安全的进行一些操作),在这个安全点上没有 Live 的引用,这种情况下 Wasmtime 会错误地使用 GC Stack map 而非 安全点。这就会导致释放一些不应该释放的内存,以及潜在的越界读写。

Wrong type for Linker-define functions when used across two Engines

Engine,是在 wasmtime 中被用于跨线程管理wasm模块的全局上下文。

Linker,是用于支持模块链接的结构。

Linker::func_* 安全函数中发现了一个问题。wasmtime 不支持函数的跨 engine 使用,这可能导致函数指针的类型混乱,导致能够安全地调用一个类型错误的函数。这种情况应该 panic!

RUSTSEC-2021-0098: Vulnerability in openssl-src

openssl-src 是用于构建 OpenSSL 给 openssl-sys 库使用的。OpenSSL 最近又发现了很多新的安全缺陷,也记录到这里了。

具体这个漏洞是指 处理ASN.1字符串时的读取缓冲区超限问题。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:denial-of-service / crypto-failure

  • CVE 编号:CVE-2021-3712

  • 详细:https://www.openssl.org/news/secadv/20210824.txt

  • 补丁:>=111.16

漏洞分析

ASN.1字符串在OpenSSL内部被表示为一个ASN1_STRING结构,它包含一个容纳字符串数据的缓冲区和一个容纳缓冲区长度的字段。这与普通的C语言字符串不同,后者表示为一个字符串数据的缓冲区,以NUL(0)字节结束。

虽然不是严格的要求,但使用OpenSSL自己的 "d2i "函数(和其他类似的解析函数)解析的ASN.1字符串,以及任何用ASN1_STRING_set()函数设置值的字符串,都会在ASN1_STRING结构中以NUL结束字节数。

然而,应用程序有可能直接构建有效的ASN1_STRING结构,通过直接设置ASN1_STRING数组中的 "data "和 "length "字段,不以NUL方式终止字节数组。这也可以通过使用ASN1_STRING_set0()函数来实现。

许多打印ASN.1数据的OpenSSL函数被认为ASN1_STRING字节数组将以NUL结尾,尽管这对直接构建的字符串来说是不保证的。如果应用程序要求打印一个ASN.1结构,而该ASN.1结构包含由应用程序直接构建的ASN1_STRING,而没有以NUL结束 "data "字段,那么就会发生读取缓冲区超限。

如果一个恶意行为者可以使一个应用程序直接构建一个ASN1_STRING,然后通过受影响的OpenSSL函数之一处理它,那么这个问题可能会被击中。这可能导致崩溃(造成拒绝服务攻击,DOS)。它还可能导致私人内存内容(如私人密钥或敏感明文)的泄露。

其他 OpenSSL 问题

OpenSSL 缺陷列表: https://rustsec.org/packages/openssl-src.html

RUSTSEC-2021-0082: Unsoundness in vec-const

vec-const试图从一个指向常量切片的指针构造一个Vec

漏洞描述:

  • 漏洞类型:Unsound

  • 漏洞分类:memory-corruption

  • CVE 编号:CVE-2021-3711

  • 详细:https://github.com/Eolu/vec-const/issues/1#issuecomment-898908241

  • 补丁:暂无,不建议使用该 crate

  • 关键字: memory-safety

漏洞分析

这个crate 违反了Rust的规则,使用起来会有危害。你不应该使用这个crate。这个crate不应该存在。它创建了不健全的抽象,允许不安全的代码伪装成安全代码。

这个crate声称要构造一个长度和容量都不为零的const Vec,但这是做不到的,因为这样的Vec需要一个来自分配器(allocator)的指针。参见:https://github.com/rust-lang/const-eval/issues/20。

RUSTSEC-2021-0093: Vulnerability in crossbeam-deque

crossbeam-deque中发生了数据竞争。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:memory-corruption

  • CVE 编号: GHSA-pqqp-xmhj-wgcw、 CVE-2021-32810

  • 详细:https://github.com/Eolu/vec-const/issues/1#issuecomment-898908241

  • 补丁:>=0.7.4, <0.8.0 / >=0.8.1

漏洞分析

在受影响的版本中,队列的一个或多个任务会被弹出两次,如果在堆上分配,会导致 dobule free 和 内存泄漏。如果不是堆上分配,则会引起逻辑错误。

修复PR :https://github.com/crossbeam-rs/crossbeam/pull/726

问题是因为任务窃取相关条件判断错误导致的,是逻辑 Bug。

RUSTSEC-2021-0077: Vulnerability in better-macro

better-macro 是一个假的 crate,它在 "证明一个观点",即proc-macros可以运行任意的代码。这是一个特别新颖或有趣的观察。

它目前打开的 https://github.com/raycar5/better-macro/blob/master/doc/hi.md,似乎没有任何恶意的内容,但不能保证会一直如此。

这个 crate 没有任何有用的功能,不应该被使用。


#![allow(unused)]
fn main() {
#[proc_macro]
pub fn println(input: TokenStream) -> TokenStream {
    if let Ok(_) = Command::new("xdg-open").arg(URL).output() {
    } else if let Ok(_) = Command::new("open").arg(URL).output() {
    } else if let Ok(_) = Command::new("explorer.exe").arg(URL).output() {
    }
    let input: proc_macro2::TokenStream = input.into();
    let out = quote! {::std::println!(#input)};
    out.into()
}
}

RUSTSEC-2021-0106: Vulnerability in bat

bat 中存在不受控制的搜索路径元素,可能会导致非预期代码执行。

0.18.2之前的windows系统中的bat会从当前工作目录中执行名为less.exe的程序。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:code-execution

  • CVE 编号:CVE-2021-36753 、 GHSA-p24j-h477-76q3

  • 详细:https://nvd.nist.gov/vuln/detail/CVE-2021-36753

  • 补丁:>=0.18.2

  • 平台: Windows

漏洞分析

修复 PR: https://github.com/sharkdp/bat/pull/1724

对传入的 Path 进行了合法验证。使用的库是 grep_cli

RUSTSEC-2021-0073: Vulnerability in prost-types

prost_types::TimestampSystemTime的转换可能导致溢出和恐慌。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:denial-of-service

  • CVE 编号:CVE-2021-36753 、 GHSA-p24j-h477-76q3

  • 详细:https://github.com/tokio-rs/prost/issues/438

  • 补丁:>=0.8.0

漏洞分析

prost-types 0.7.0中,从TimestampSystemTime的转换使用UNIX_EPOCH上的+-运算符。如果输入的Timestamp是不被信任的,这可能会溢出和恐慌,造成拒绝服务的漏洞。因为 SystimeTime 内部实现的 +- 使用 checked_add/checked_sub会发生 panic。


#![allow(unused)]
fn main() {
use prost_types::Timestamp;
use std::time::SystemTime;

SystemTime::from(Timestamp {
    seconds: i64::MAX,
    nanos: 0,
}); // panics on i686-unknown-linux-gnu (but not x86_64) with default compiler settings

SystemTime::from(Timestamp {
    seconds: i64::MAX,
    nanos: i32::MAX,
}); // panics on x86_64-unknown-linux-gnu with default compiler settings
}

另外,转换涉及到调用Timestamp::normalize,它使用了+-运算符。这可能会引起恐慌或环绕(wrap around,取决于编译器设置),如果应用程序被编译为溢出时恐慌,也会产生拒绝服务的漏洞。

解决问题的思路是:

Timestamp::normalize可能应该使用 saturating_{add,sub} 方法,如果时间戳的nanos字段超出了范围,这可能会默默地改变时间戳,最多3秒,但这样的时间戳可以说是无效的,所以这可能是好的。

SystemTime 没有Saturating_{add,sub}方法,也没有MINMAX常数,应该再次使用 SystemTime::checked_{add,sub} 进行转换。

修复 PR: https://github.com/tokio-rs/prost/pull/439

RUSTSEC-2021-0078: Vulnerability in hyper

对Content-Length进行宽松的 header 解析,可能会使请求被偷渡(走私,smuggling)。

背景: 请求偷渡

不合法的请求被夹杂在合法请求中被得到处理。需要通过 content-lengthTransfer-Encoding 两个header 来构造攻击。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:http、parsing

  • CVE 编号:CVE-2021-32715

  • 详细:https://github.com/hyperium/hyper/security/advisories/GHSA-f3pg-qwvg-p99c

  • 补丁:>=0.14.10

漏洞分析

hyper的HTTP/1服务器代码存在一个缺陷,即错误地解析和接受带有前缀加号的Content-Length头的请求,而这一请求本应作为非法请求被拒绝。这与上游HTTP代理不解析这种Content-Length头而转发的情况相结合,可能导致 "请求偷渡("request smuggling) "或 "去同步攻击(desync attacks)"。

修复代码:https://github.com/hyperium/hyper/commit/06335158ca48724db9bf074398067d2db08613e7

需要判断 content-lenght 是不是可以正常转换为有效数位。

RUSTSEC-2021-0072: Vulnerability in tokio

当用JoinHandle::abort中止一个任务时,对于 LocalSet上生成的任务不正确, 容易导致竞态条件。

漏洞描述:

  • 漏洞类型:Vulnerability

  • 漏洞分类:memory-corruption

  • CVE 编号:CVE-2021-32715

  • 详细:https://github.com/tokio-rs/tokio/issues/3929

  • 补丁:

    >=1.5.1, <1.6.0>=1.6.3, <1.7.0>=1.7.2, <1.8.0>=1.8.1

漏洞分析

当用JoinHandle::abort中止一个任务时,如果该任务当前没有被执行,那么在调用abort的线程中,Future会被 Drop。这对于在LocalSet上生成的任务是不正确的。

这很容易导致竞态条件,因为许多项目在它们的Tokio任务中使用RcRefCell以获得更好的性能。

修复 PR: https://github.com/tokio-rs/tokio/pull/3934

RUSTSEC-2021-0070: Vulnerability in nalgebra

nalgebra 库中 VecStorageDeserialize实现没有保持元素数量必须等于nrows * ncols的不变性。对特制的输入进行反序列化时,可能会允许超出向量分配的内存访问。

漏洞描述:

  • 漏洞类型:Vulnerability
  • 漏洞分类:memory-corruption/ memory-exposure
  • CVE 编号:CVE-2021-32715
  • 详细:https://github.com/dimforge/nalgebra/issues/883
  • 补丁:>=0.27.1

漏洞分析

这个缺陷是在v0.11.0(086e6e)中引入的,因为为MatrixVec增加了一个自动派生(derive)的Deserialize实现。MatrixVec后来在v0.16.13(0f66403)中被改名为VecStorage,并继续使用自动派生的Deserialize实现。

修复 PR : https://github.com/dimforge/nalgebra/pull/889

在反序列化的过程中,对 nrows.value() * ncols.value() == data.len() 进行校验。

CVE-2021-31162: Vulnerability in std

在 Rust 1.52.0之前的Rust标准库中,如果释放元素时出现panic ,在Vec::from_iter函数中会出现 double free。

漏洞描述:

  • 漏洞类型:Vulnerability
  • 漏洞分类:memory-corruption
  • CVE 编号:CVE-2021-31162
  • 详细:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-31162
  • 补丁:>=1.52.0

漏洞分析

漏洞复现代码:

use std::iter::FromIterator;

#[derive(Debug)]
enum MyEnum {
    DroppedTwice(Box<i32>),
    PanicOnDrop,
}

impl Drop for MyEnum {
    fn drop(&mut self) {
        match self {
            MyEnum::DroppedTwice(_) => println!("Dropping!"),
            MyEnum::PanicOnDrop => {
                if !std::thread::panicking() {
                    panic!();
                }
            }
        }
    }
}

fn main() {
    let v = vec![MyEnum::DroppedTwice(Box::new(123)), MyEnum::PanicOnDrop];
    Vec::from_iter(v.into_iter().take(0));
}

// Output : free(): double free detected in tcache 2

因为枚举MyEnum在 析构的时候panic,导致资源泄漏,而引发了双重 free 的问题。

修复 PR: https://github.com/rust-lang/rust/pull/84603

Vec::from_iter 中执行 forget_allocation_drop_remaining,即,忘记已经被drop的src的元素分配的内存,即便 drop 发生了 panic,也不会泄漏资源。