作者:張漢東
原文鏈接:https://mp.weixin.qq.com/s/y-APUMBid3J02UDUf-L5jw
相關閱讀:Rust生態安全漏洞總結系列 | Part 1

本系列主要是分析RustSecurity 安全數據庫庫[1]中記錄的Rust生態社區中發現的安全問題,從中總結一些教訓,學習Rust安全編程的經驗。

本期分析了下面六個安全問題:

  • RUSTSEC-2021-0067 : Cranelift 模塊中代碼生成缺陷導致可能的 WASM 沙箱逃逸
  • RUSTSEC-2021-0054:rkyv crate 可能包含未初始化的內存
  • RUSTSEC-2021-0041:parse_duration 通過用太大的指數解析 Payload 來拒絕服務(DOS)
  • RUSTSEC-2021-0053:算法庫中 merge_sort::merge() 導致實現 Drop 的類型 雙重釋放( double-free)
  • RUSTSEC-2021-0068: iced x86 版本中 不合理(Soundness) 的問題
  • RUSTSEC-2021-0037:Diesel 庫的 Sqlite 后端 UAF(use-after-free) bug

看是否能給我們一些啟示。


RUSTSEC-2021-0067 : Cranelift 模塊中代碼生成缺陷導致可能的 WASM 沙箱逃逸

在 Cranelift 中發現了一個漏洞。具有未知輸入的操作導致特權升級漏洞。CWe正在將問題分類為CWE-264。這將對機密性,完整性和可用性產生影響。

漏洞描述:

Cranelift X64后端的0.73.0中有一個錯誤,可以創建一個可能導致 Webassembly 模塊中的潛在沙箱逃逸(sandbox escape )的場景。版本0.73.0的Cranelift的用戶應升級到0.73.10.74,以修復此漏洞。

如果未使用舊的默認后端,則在0.73.0之前的 Cranelift 用戶應該更新為0.73.10.74

漏洞分析

此問題是在 Cranelift 新后端中引入的(Cranelift 經歷過大的重構)。

一些背景:寄存器分配如果物理寄存器的數量不足以滿足虛擬寄存器的需求,有些虛擬寄存器顯然就只能映射到內存。這些虛擬寄存器稱為溢出(spill)虛擬寄存器。寄存器分配算法的好壞直接決定了程序中寄存器的利用率。 Cranelift 寄存器分配相關文章:https://cfallin.org/blog/2021/03/15/cranelift-isel-3/[3]該文章還詳細介紹了該團隊如何保證 Cranelift 生成正確的代碼。即便如此,還是產生了邏輯 Bug。

這個 Bug 是一個邏輯 Bug:

原因是,寄存器分配器重新加載比 64位 窄的溢出(spill)整數值時,從棧上加載的值執行了符號擴展而不是零擴展。

這對另一個優化產生了糟糕的影響:當我們知道產生32位值的指令實際上將其目標寄存器的高32位置零時,指令選擇器將選擇一個32到64位的零擴展運算符。因此,我們依賴于這些歸零位,但值的類型仍然是I32,并且溢出/重新加載將這些比特位重構為I32的MSB的符號擴展。

所以,在某些特定情況下,如果i32值為指針,則可能會出現沙箱逃逸的情況。為堆訪問發出的常規代碼對 WebAssembly 堆地址進行零擴展,將其添加到64位堆基,然后訪問結果地址。如果零擴展成為符號擴展,則模塊可以在堆開始之前向后訪問并訪問最大2GiB的內存。

符號擴充 (sign-extend): 指在保留數字的符號(正負性)及數值的情況下,增加二進制數字位數的操作。 零擴充(zero-extend):用于將無符號數字移動至較大的字段中,同時保留其數值。

該 Bug 的影響力依賴于堆的實現。具體而言:

如果堆有邊界檢查。并且,不完全依賴于保護頁面。并且堆綁定為2GiB或更小。則該 Bug 無法用于從另一個 WebAssembly 模塊堆訪問內存。

如果使用此 Bug 可訪問的范圍中沒有映射內存,例如,如果 WebAssembly 模塊堆之前有 2 GiB 保護區域,則可以減輕此漏洞的影響。

RUSTSEC-2021-0054:rkyv crate 可能包含未初始化的內存

漏洞描述:

rkyv是一個序列化框架 在序列化期間,可能無法初始化結構填充字節和未使用的枚舉字節。這些字節可以寫入磁盤或發送不安全的通道。

漏洞分析

補丁代碼:https://github.com/djkoloski/rkyv/commit/9c65ae9c2c67dd949b5c3aba9b8eba6da802ab7e[7]

有問題的代碼:

unsafe fn resolve_aligned<T: Archive + ?Sized>(
        &mut self,
        value: &T,
        resolver: T::Resolver,
    ) -> Result<usize, Self::Error> {
    // ...
    let mut resolved = mem::MaybeUninit::zeroed();
    // ...
}

mem::MaybeUninit::zeroed()函數會創建一個新的MaybeUninit<T>實例,并且該內存位會被填充0。但是這依賴于 T是否能被正確初始化。比如:MaybeUninit<usize>::zeroed()是初始化,但是MaybeUninit<&'static i32>::zeroed()就沒有被正確初始化。這是因為 Rust 里引用不能為空。

所以,現在這個 resolver 是個泛型 T,不一定能正確初始化,所以有未初始化的風險。

修復之后的代碼:

    let mut resolved = mem::MaybeUninit::<T::Archived>::uninit();
    resolved.as_mut_ptr().write_bytes(0, 1);

直接假設其沒有正確初始化,然后使用write_bytes手工將其初始化,確保正確。

RUSTSEC-2021-0041:parse_duration 通過用太大的指數解析 Payload 來拒絕服務(DOS)

漏洞描述:

漏洞解析

parse_duration 庫用來將字符串解析為持續時間(duration)。

問題代碼:

if exp < 0 {
    boosted_int /= pow(BigInt::from(10), exp.wrapping_abs() as usize);
} else {
    boosted_int *= pow(BigInt::from(10), exp.wrapping_abs() as usize);
}
duration.nanoseconds += boosted_int;

此為 parse 函數內的代碼片段,允許使用指數級的持續時間字符串解析,其中BigInt 類型與 pow 功能一起用于這類 Payload。該功能會導致長時間占用CPU和內存。

這允許攻擊者使用 parse 功能來制造 DOS 攻擊。雖然該庫已經不維護了,而且star數也不多,但是不清楚依賴它的庫有多少,可以使用 cargo-audit 來檢查你項目里的依賴。

RUSTSEC-2021-0053:算法庫中 merge_sort::merge()導致實現 Drop 的類型 雙重釋放( double-free)

漏洞分析

algorithmica[10]是 Rust 實現算法的教學庫,網站為:https://www.fifthtry.com/abrar/rust-algorithms/[11]。

該庫中的歸并排序的實現中,merge 函數導致 對列表元素持有雙份所有權,所以會雙重釋放(double free)。

注意下面源碼中,為 unsafe rust 實現。

 fn merge<T: Debug, F>(list: &mut [T], start: usize, mid: usize, end: usize, compare: &F) 
 where 
     F: Fn(&T, &T) -> bool, 
 { 
     let mut left = Vec::with_capacity(mid - start + 1); 
     let mut right = Vec::with_capacity(end - mid); 
     unsafe { 
         let mut start = start; 
         while start <= mid { 
             left.push(get_by_index(list, start as isize).read()); 
             start += 1; 
         } 
         while start <= end { 
             right.push(get_by_index(list, start as isize).read()); 
             start += 1; 
         } 
     } 

     let mut left_index = 0; 
     let mut right_index = 0; 
     let mut k = start; 

     unsafe { 
         while left_index < left.len() && right_index < right.len() { 
             if compare(&left[left_index], &right[right_index]) { 

                 // 通過 `list[k] = ` 這種方式重復持有元素所有權
                 list[k] = get_by_index(&left, left_index as isize).read(); 

                 left_index += 1; 
             } else { 
                 list[k] = get_by_index(&right, right_index as isize).read(); 
                 right_index += 1; 
             } 
             k += 1; 
         } 

         while left_index < left.len() { 
             list[k] = get_by_index(&left, left_index as isize).read(); 
             left_index += 1; 
             k += 1; 
         } 

         while right_index < right.len() { 
             list[k] = get_by_index(&right, right_index as isize).read(); 
             right_index += 1; 
             k += 1; 
         } 
     } 
 } 

unsafe fn get_by_index<T>(list: &[T], index: isize) -> *const T {
    let list_offset = list.as_ptr();
    list_offset.offset(index)
}

Bug 復現:

use algorithmica::sort::merge_sort::sort;

fn main() {
    let mut arr = vec![
        String::from("Hello"),
        String::from("World"),
        String::from("Rust"),
    ];

    // Calling `merge_sort::sort` on an array of `T: Drop` triggers double drop
    algorithmica::sort::merge_sort::sort(&mut arr);
    dbg!(arr);
}

輸出:

free(): double free detected in tcache 2

Terminated with signal 6 (SIGABRT)

該 Bug 還未得到修復。

此問題給我們的啟示:不要為了刷題而忽略安全。

RUSTSEC-2021-0068: iced x86 版本中 不合理(Soundness) 的問題

漏洞描述:

漏洞分析

iced 用戶在使用 miri 編譯其項目時,發現 UB:

error: Undefined Behavior: memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3
    --> C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365:18
     |
365  |         unsafe { &*index.get_unchecked(self) }
     |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information

     = note: inside `core::slice::<impl [u8]>::get_unchecked::<usize>` at C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365:18
     = note: inside `iced_x86::Decoder::new` at C:\Users\lander\.cargo\registry\src\github.com-1ecc6299db9ec823\iced-x86-1.9.1\src\decoder\mod.rs:457:42
note: inside `Emulator::run` at src\lib.rs:563:27
    --> src\lib.rs:563:27
     |
563  |         let mut decoder = Decoder::new(self.bitness, bytes, self.decoder_options);

該用戶在使用 Decoder::new 的時候出現了 UB。在 iced相關源碼中,即 iced/src/rust/iced-x86/src/decoder.rs 中,存在

let data_ptr_end: *const u8 = unsafe { 
    data.get_unchecked(data.len()) 
}; 

根據標準庫文檔[13]描述:

Calling this method with an out-of-bounds index is undefined behavior even if the resulting reference is not used.使用 界外索引調用該方法就是 未定義行為(UB),即便這個結果的引用沒有被使用。

示例:

let x = &[1, 2, 4];

unsafe {
    assert_eq!(x.get_unchecked(1), &2);
    assert_eq!(x.get_unchecked(3), &2); // UB
}

該代碼已經被修復為,不再使用 get_unchecked :

let data_ptr_end = data.as_ptr() as usize + data.len();

RUSTSEC-2021-0037:Diesel 庫的 Sqlite 后端 UAF(use-after-free) bug

漏洞描述:

漏洞分析

Diesel 的 sqlite 后端使用了 libsqlite3_sys 這個庫來調用 sqlite 提供的sql函數。比如sqlite3_finalizesqlite3_step 之類。

sqlite 函數執行調用過程:

  • sqlite3_open()
  • sqlite3_prepare()
  • sqlite3_step() // 用于執行有前面sqlite3_prepare創建的 預編譯語句
  • sqlite3_column() // 從執行sqlite3_step()執行一個預編譯語句得到的結果集的當前行中返回一個列
  • sqlite3_finalize() // 銷毀前面被sqlite3_prepare創建的預編譯語句
  • sqlite3_close()

Diesel 的 by_name 查詢通用做法是將預編譯語句的所有字段名稱保存為字符串切片以備以后使用。

但是sqlite的行為是:

  • 返回的字符串指針一直有效,直到準備好的語句被 sqlite3_finalize() 銷毀,
  • 或者直到第一次調用 sqlite3_step() 為特定運行自動重新預編譯該語句,
  • 或者直到下一次調用 sqlite3_column_name()sqlite3_column_name16() 在同一列。

在之前版本的 Diesel 中,沒有注意到這種情況,在調用 sqlite3_step() 之后,因為重新預編譯語句,導致之前字符串切片指針就無效了。就造成 UAF 的情況。

這個案例告訴我們,在使用 FFi 的時候,要注意綁定sys庫 的相關行為。這個在 Rust 編譯器這邊是無法檢查發現的,案例應該屬于邏輯 Bug。

參考資料

[1]RustSecurity 安全數據庫庫: https://rustsec.org/advisories/

[2]https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-hpqh-2wqx-7qp5: https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-hpqh-2wqx-7qp5

[3]https://cfallin.org/blog/2021/03/15/cranelift-isel-3/: https://cfallin.org/blog/2021/03/15/cranelift-isel-3/

[4]https://github.com/bytecodealliance/wasmtime/pull/2919/files: https://github.com/bytecodealliance/wasmtime/pull/2919/files

[5]https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-hpqh-2wqx-7qp5: https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-hpqh-2wqx-7qp5

[6]https://github.com/djkoloski/rkyv/issues/113: https://github.com/djkoloski/rkyv/issues/113

[7]https://github.com/djkoloski/rkyv/commit/9c65ae9c2c67dd949b5c3aba9b8eba6da802ab7e: https://github.com/djkoloski/rkyv/commit/9c65ae9c2c67dd949b5c3aba9b8eba6da802ab7e

[8]https://github.com/zeta12ti/parse_duration/issues/21: https://github.com/zeta12ti/parse_duration/issues/21

[9]https://github.com/AbrarNitk/algorithmica/issues/1: https://github.com/AbrarNitk/algorithmica/issues/1

[10]algorithmica: https://github.com/AbrarNitk/algorithmica

[11]https://www.fifthtry.com/abrar/rust-algorithms/: https://www.fifthtry.com/abrar/rust-algorithms/

[12]https://github.com/icedland/iced/issues/168: https://github.com/icedland/iced/issues/168

[13]標準庫文檔:https://doc.rust-lang.org/std/primitive.slice.html#method.get_unchecked

[14]https://github.com/diesel-rs/diesel/pull/2663: https://github.com/diesel-rs/diesel/pull/2663


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1730/