作者:張漢東
原文鏈接:https://mp.weixin.qq.com/s/qxTGZedX21izD60EH6-Q3A
本系列主要是分析RustSecurity 安全數據庫庫中記錄的Rust生態社區中發現的安全問題,從中總結一些教訓,學習Rust安全編程的經驗。
作為本系列文章的首篇文章,我節選了RustSecurity 安全數據庫庫中 2021 年 1 月份記錄的前五個安全漏洞來進行分析。
01 Mdbook XSS 漏洞 (RUSTSEC-2021-0001)

正好《Rust 中文精選(RustMagazine)》也用了 mdbook,不過讀者朋友不用害怕,本刊用的 mdbook 是修補了該漏洞的版本。
該漏洞并非 Rust 導致,而是生成的網頁中 JS 函數使用錯誤的問題。
漏洞描述:
問題版本的 mdBook 中搜索功能(在版本0.1.4中引入)受到跨站點腳本漏洞的影響,該漏洞使攻擊者可以通過誘使用戶鍵入惡意搜索查詢或誘使用戶進入用戶瀏覽器來執行任意JavaScript代碼。
漏洞成因分析:
XSS的漏洞主要成因是后端接收參數時未經過濾,導致參數改變了HTML的結構。而mdbook中提供的js函數encodeURIComponent會轉義除'之外的所有可能允許XSS的字符。因此,還需要手動將'替換為其url編碼表示形式(%27)才能解決該問題。
修復 PR 也很簡單。
02 暴露裸指針導致段錯誤 (RUSTSEC-2021-0006)
該漏洞誕生于第三方庫cache,該庫雖然已經兩年沒有更新了,但是它里面出現的安全漏洞的警示作用還是有的。該庫問題issue中說明了具體的安全漏洞。
該安全漏洞的特點是,因為庫接口中將裸指針(raw pointer) 公開了出來,所以該裸指針可能被用戶修改為空指針,從而有段錯誤風險。因為這個隱患是導致 Safe Rust 出現 UB,所以是不合理的。
以下代碼的注釋分析了漏洞的產生。
use cache;
/**
`cache crate` 內部代碼:
```rust
pub enum Cached<'a, V: 'a> {
/// Value could not be put on the cache, and is returned in a box
/// as to be able to implement `StableDeref`
Spilled(Box<V>),
/// Value resides in cache and is read-locked.
Cached {
/// The readguard from a lock on the heap
guard: RwLockReadGuard<'a, ()>,
/// A pointer to a value on the heap
// 漏洞風險
ptr: *const ManuallyDrop<V>,
},
/// A value that was borrowed from outside the cache.
Borrowed(&'a V),
}
**/
fn main() {
let c = cache::Cache::new(8, 4096);
c.insert(1, String::from("test"));
let mut e = c.get::<String>(&1).unwrap();
match &mut e {
cache::Cached::Cached { ptr, .. } => {
// 將 ptr 設置為 空指針,導致段錯誤
*ptr = std::ptr::null();
},
_ => panic!(),
}
// 輸出:3851,段錯誤
println!("Entry: {}", *e);
}
啟示:
所以,這里我們得到一個教訓,就是不能隨便在公開的 API 中暴露裸指針。值得注意的是,該庫處于失去維護狀態,所以這個漏洞還沒有被修正。
03 讀取未初始化內存導致UB (RUSTSEC-2021-0008)

該漏洞誕生于 bra 庫。該庫這個安全漏洞屬于邏輯 Bug 。因為錯誤使用 標準庫 API,從而可能讓用戶讀取未初始化內存導致 UB。
披露該漏洞的issue。目前該漏洞已經被修復。
以下代碼注釋保護了對漏洞成因對分析:
// 以下是有安全風險的代碼示例:
impl<R> BufRead for GreedyAccessReader<R>
where
R: Read,
{
fn fill_buf(&mut self) -> IoResult<&[u8]> {
if self.buf.capacity() == self.consumed {
self.reserve_up_to(self.buf.capacity() + 16);
}
let b = self.buf.len();
let buf = unsafe {
// safe because it's within the buffer's limits
// and we won't be reading uninitialized memory
// 這里雖然沒有讀取未初始化內存,但是會導致用戶讀取
std::slice::from_raw_parts_mut(
self.buf.as_mut_ptr().offset(b as isize),
self.buf.capacity() - b)
};
match self.inner.read(buf) {
Ok(o) => {
unsafe {
// reset the size to include the written portion,
// safe because the extra data is initialized
self.buf.set_len(b + o);
}
Ok(&self.buf[self.consumed..])
}
Err(e) => Err(e),
}
}
fn consume(&mut self, amt: usize) {
self.consumed += amt;
}
}
GreedyAccessReader::fill_buf方法創建了一個未初始化的緩沖區,并將其傳遞給用戶提供的Read實現(self.inner.read(buf))。這是不合理的,因為它允許Safe Rust代碼表現出未定義的行為(從未初始化的內存讀取)。
在標準庫Read trait 的 read 方法文檔中所示:
您有責任在調用
read之前確保buf已初始化。用未初始化的buf(通過MaybeUninit <T>獲得的那種)調用read是不安全的,并且可能導致未定義的行為。https://doc.rust-lang.org/std/io/trait.Read.html#tymethod.read
解決方法:
在read之前將新分配的u8緩沖區初始化為零是安全的,以防止用戶提供的Read讀取新分配的堆內存的舊內容。
修正代碼:
// 修正以后的代碼示例,去掉了未初始化的buf:
impl<R> BufRead for GreedyAccessReader<R>
where
R: Read,
{
fn fill_buf(&mut self) -> IoResult<&[u8]> {
if self.buf.capacity() == self.consumed {
self.reserve_up_to(self.buf.capacity() + 16);
}
let b = self.buf.len();
self.buf.resize(self.buf.capacity(), 0);
let buf = &mut self.buf[b..];
let o = self.inner.read(buf)?;
// truncate to exclude non-written portion
self.buf.truncate(b + o);
Ok(&self.buf[self.consumed..])
}
fn consume(&mut self, amt: usize) {
self.consumed += amt;
}
}
啟示:
該漏洞給我們對啟示是,要寫出安全的 Rust 代碼,還必須掌握每一個標準庫里 API 的細節。否則,邏輯上的錯誤使用也會造成UB。
04 讀取未初始化內存導致UB (RUSTSEC-2021-0012)

該漏洞誕生于第三方庫[cdr-rs]中,漏洞相關issue中。
該漏洞和 RUSTSEC-2021-0008 所描述漏洞風險是相似的。
cdr-rs 中的 Deserializer::read_vec方法創建一個未初始化的緩沖區,并將其傳遞給用戶提供的Read實現(self.reader.read_exact)。
這是不合理的,因為它允許安全的Rust代碼表現出未定義的行為(從未初始化的內存讀取)。
漏洞代碼:
fn read_vec(&mut self) -> Result<Vec<u8>> {
let len: u32 = de::Deserialize::deserialize(&mut *self)?;
// 創建了未初始化buf
let mut buf = Vec::with_capacity(len as usize);
unsafe { buf.set_len(len as usize) }
self.read_size(u64::from(len))?;
// 將其傳遞給了用戶提供的`Read`實現
self.reader.read_exact(&mut buf[..])?;
Ok(buf)
}
修正:
fn read_vec(&mut self) -> Result<Vec<u8>> {
let len: u32 = de::Deserialize::deserialize(&mut *self)?;
// 創建了未初始化buf
let mut buf = Vec::with_capacity(len as usize);
// 初始化為 0;
buf.resize(len as usize, 0);
self.read_size(u64::from(len))?;
// 將其傳遞給了用戶提供的`Read`實現
self.reader.read_exact(&mut buf[..])?;
Ok(buf)
}
啟示:同上。
05 Panic Safety && Double free (RUSTSEC-2021-0011)

以下兩段代碼是漏洞展示,注意注釋部分都解釋:
//case 1
macro_rules! from_event_option_array_into_event_list(
($e:ty, $len:expr) => (
impl<'e> From<[Option<$e>; $len]> for EventList {
fn from(events: [Option<$e>; $len]) -> EventList {
let mut el = EventList::with_capacity(events.len());
for idx in 0..events.len() {
// 這個 unsafe 用法在 `event.into()`調用panic的時候會導致雙重釋放
let event_opt = unsafe { ptr::read(events.get_unchecked(idx)) };
if let Some(event) = event_opt { el.push::<Event>(event.into()); }
}
// 此處 mem::forget 就是為了防止 `dobule free`。
// 因為 `ptr::read` 也會制造一次 drop。
// 所以上面如果發生了panic,那就相當于注釋了 `mem::forget`,導致`dobule free`
mem::forget(events);
el
}
}
)
);
// case2
impl<'e, E> From<[E; $len]> for EventList where E: Into<Event> {
fn from(events: [E; $len]) -> EventList {
let mut el = EventList::with_capacity(events.len());
for idx in 0..events.len() {
// 同上
let event = unsafe { ptr::read(events.get_unchecked(idx)) };
el.push(event.into());
}
// Ownership has been unsafely transfered to the new event
// list without modifying the event reference count. Not
// forgetting the source array would cause a double drop.
mem::forget(events);
el
}
}
以下是一段該漏洞都復現代碼(我本人沒有嘗試過,但是提交issue都作者試過了),注意下面注釋部分的說明:
// POC:以下代碼證明了上面兩個case會發生dobule free 問題
use fil_ocl::{Event, EventList};
use std::convert::Into;
struct Foo(Option<i32>);
impl Into<Event> for Foo {
fn into(self) -> Event {
/*
根據文檔,`Into <T>`實現不應出現 panic。但是rustc不會檢查Into實現中是否會發生恐慌,
因此用戶提供的`into()`可能會出現風險
*/
println!("LOUSY PANIC : {}", self.0.unwrap()); // unwrap 是有 panic 風險
Event::empty()
}
}
impl Drop for Foo {
fn drop(&mut self) {
println!("I'm dropping");
}
}
fn main() {
let eventlist: EventList = [Foo(None)].into();
dbg!(eventlist);
}
以下是 Fix 漏洞的代碼,使用了ManuallyDrop,注意注釋說明:
macro_rules! from_event_option_array_into_event_list(
($e:ty, $len:expr) => (
impl<'e> From<[Option<$e>; $len]> for EventList {
fn from(events: [Option<$e>; $len]) -> EventList {
let mut el = ManuallyDrop::new(
EventList::with_capacity(events.len())
);
for idx in 0..events.len() {
let event_opt = unsafe {
ptr::read(events.get_unchecked(idx))
};
if let Some(event) = event_opt {
// Use `ManuallyDrop` to guard against
// potential panic within `into()`.
// 當 into 方法發生 panic 當時候,這里 ManuallyDrop 可以保護其不會`double free`
let event = ManuallyDrop::into_inner(
ManuallyDrop::new(event)
.into()
);
el.push(event);
}
}
mem::forget(events);
ManuallyDrop::into_inner(el)
}
}
)
);
啟示:
在使用 std::ptr 模塊中接口需要注意,容易產生 UB 問題,要多多查看 API 文檔。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1728/
暫無評論