研究了rust调用/被调用外部库中堆块内存传递造成的安全问题
问题
rust调用外部函数库比如C/C++写的库可能会导致不安全,即使外部包是纯rust编写的包(不使用FFI),也可能存在漏洞
现在又很多方法来增强FFI的安全性,比如rust-bndgen,safer_ffi
但是都只能帮助编写正确的数据接口。在跨过FFI使用堆分配和free造成的内存漏洞仍然是一个未解决的问题
此外,Rust有一个独特的内存管理所有权系统,它创造了自己的内存安全问题范式。因此,现有的关于其他内存安全编程语言滥用FFI的工作,如Java和Python,已不再适用。
背景知识
rust所有权,借用和生命周期
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
借用
引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给 calculate_length,同时在函数定义中,我们获取 &String 而不是 String。这些 & 符号就是 引用,它们允许你使用值但不获取其所有权。
可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
引用还不能悬垂引用和跨过作用域
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
现有工作
rust静态分析
许多现有的研究扩展了现成的静态分析引擎,对Rust编译器生成的LLVM IR进行错误检测。Lindner等人使用符号执行引擎KLEE来验证一个程序是否无panic-free
SMACK将LLVM IR翻译成Boogie中间验证语言。
Rust2Viper和Prusti利用用户提供的规范和Viper符号执行引擎来验证功能正确性
CRUST将包含unsafe代码的函数翻译成C语言,然后生成测试并由CBMC模型检查器检查。
还有不少研究是在rust的中间语言上MIR上做分析,比如MirChecker,Safe Drop等工具
跨语言缺陷检测与预防
Mergendahl等人提出了一个威胁模型来推理跨语言的攻击。他们还在Rust和Go上演示了这些攻击。
Kondoh等人使用静态分析来检测使用Java Native Interface(JNI)时的常见错误和不良编程实践。
Tan等人应用静态分析,对Sun公司的Java开发包(JDK)中的一部分本地代码进行了经验性的安全研究。
JET是一个静态分析工具,它对通过JNI在本地代码中引发的Java异常执行异常检查并报告错误。
JET将一个JNI代码包作为输入,它由Java类文件和本地代码组成。该包被输入到两个阶段的异常分析。对于每个本地方法实现,异常分析将输出其实际的异常效果。然后,JET将实际效果与从Java类文件中提取的已声明的异常效果进行比较。如果出现不匹配,警告生成器会发出警告。请注意,我们的分析并没有检查Java代码,而只检查Java类文件中的本机代码和类型签名。
Jinn [18] 是一个独立于编译器和虚拟机的错误检测工具,适用于JNI和Python/C。
Galeed[33]和PKRU-Safe[15]使用英特尔内存保护密钥(MPK)在运行时隔离堆内存,这样,不安全(外部)代码就不能破坏安全语言组件专用的内存。
由Rust所有权系统和C/C++之间的互动所引入的新的错误模式超出了所有现有的检测或预防工作的范围。
本文研究点
研究了跨FFI边界的堆内存管理问题的安全影响,特别是那些由Rust的基于所有权的内存管理和C/C++的手动内存管理相结合造成的问题。
使用静态分析技术来检测跨越FFI边界的潜在内存管理错误。基于抽象解释的理论。设计了一个增强的污点分析算法来跟踪堆内存的状态,它捕捉了基于所有权的内存管理所产生的范式。我们实现了名为FFIChecker的工具,它自动收集所有生成的Rust和C/C++代码的LLVM中间代码(IR),然后进行静态分析并输出诊断报告。然后,安全分析师可以检查这些报告,并确定是否有任何真正的bug。我们的评估表明,FFIChecker 可以在可接受的时间内以合理的精度成功地检测出内存安全问题。据我们所知,我们的工作是第一个解决Rust程序中跨FFI边界的内存管理问题的努力。我们将我们的贡献总结如下。
- 展示了当程序员通过FFI将Rust和C/C++混合在一起时,潜在的安全和内存管理问题
- 我们提出了一个在所有权原则内存管理方案中捕获内存状态的增强抽象域。
- 设计并构建了FFIChecker,这是一个自动化的静态分析器,可以检测Rust包中跨越FFI边界的潜在内存管理错误,并报告信息性的诊断消息。源代码可在线获取,这可以作为未来其他研究的基础。
- 我们在Rust生态系统中进行了广泛的评估。我们评估了从官方软件包注册表中抓取的987个软件包,在12个软件包中发现了34个bug。所有检测到的bug都经过人工确认并报告给了作者,其中15个bug在写作时已经被修复。
证明FFI是不安全的参考[12,41,24]
官方Rust包注册表(crates.io)上超过72%的包至少依赖于一个不安全的FFI-绑定包
漏洞类型
在本文中,我们只考虑在Rust中分配堆内存并传递给C/C++的情况
能分析出来的漏洞为三种:
- 常见内存损坏
Box::into_raw会将指向堆块的智能指针变成裸指针,然后将裸指针传入FFI中,同时这个堆块也就不属于Box来管理了,需要程序员手动管理,emd库就没有管理裸指针,造成了内存泄露。
let mut cost = Vec::with_capacity(X.rows());
for x in X.outer_iter() {
let mut cost_i = Vec::with_capacity(Y.rows()); // Allocate a vector
for y in Y.outer_iter() {
cost_i.push(distance(&x, &y) as c_double);
}
// Forget the memory using `Box::into_raw`
cost.push(Box::into_raw(cost_i.into_boxed_slice()) as *const c_double);
}
// Call FFI function
let d = unsafe {
emd(X.rows(),
weight_x.as_ptr(),
Y.rows(),
weight_y.as_ptr(),
cost.as_ptr(),
null())
};
- 异常安全
rust没有try-catch,rust的异常处理机制是,所有可恢复的错误必须被处理或传播回调用者函数,而所有不可恢复的错误则通过终止执行和解开堆栈来处理。所有堆栈对象的析构器将在解开堆栈时被调用,以防止资源泄漏。
然而,当跨越FFI边界传递堆内存并与外部代码合作时,开发人员通常必须通过不安全的代码暂时创建不健全的状态(例如,创建暂时未初始化的数据)。然后在外部代码完成后,开发人员手动清理这些状态。如果中间发生了一些错误,执行就会停止,堆栈就会解开,所以清理程序就不会被执行。剩余的不健全状态可能会导致安全问题。下面的代码是在libtaos库中出现的此问题。
3-8行的不安全块中,内存被传递给FFI。注意,第5行和第7行的问号运算符(?)意味着如果操作失败,函数会提前返回并将错误传播给调用者函数。因此,如果函数提前返回,内存可能被泄露,因此第10行的free函数将不会被调用。
pub fn bind(&mut self, params: impl IntoParams) -> Result<(), TaosError> {
let params = params.into_params();
unsafe {
let res = taos_stmt_bind_param(self.stmt, params.as_ptr() as _);
self.err_or(res)?;
let res = taos_stmt_add_batch(self.stmt)