G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker

渗透技巧 2年前 (2022) admin
478 0 0
今天为大家推荐的是一篇由香港中文大学完成并投稿的一篇关于Rust和C/C++跨语言调用静态分析的文章,Detecting Cross-Language Memory Management Issues in Rust。该工作目前已发表于安全会议ESORICS 2022。
G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker
Rust作为一种系统编程语言,以其强大的类型系统和独特的所有权(ownership)机制避免了大部分的内存安全问题,已经被广泛应用于系统级编程。现实中的软件项目往往同时使用Rust和其他不安全的语言(例如C/C++)以达到安全和开发效率的平衡,例如Firefox,Google Fuchsia OS等。然而Rust中的跨语言调用是不安全的,在这篇文章中,作者揭示了跨语言调用所引发的内存安全问题和内存管理问题,同时作者提出了一种静态分析的方法自动化地检测相关漏洞。

研究背景
在Rust越来越流行的今天,许多公司和开源社区都在尝试用Rust重写其软件的部分模块以提升安全性。例如Firefox原本主要由C/C++开发,如今已包含了大量的Rust代码;原本由纯C开发的Linux内核也正在逐步支持用Rust编写内核模块。对于新开发的Rust项目,也经常需要链接已有的C/C++库以避免重复造轮子。这就导致了现实中的Rust开发往往混合使用Rust和其他不安全的语言。为此,Rust提供了外部语言函数接口(Foreign Function Interface,简称FFI)来支持对其他语言的调用和数据交换。
然而在Rust中使用FFI调用外部函数(Foreign Function)是不安全(unsafe)的,因为Rust编译器无法对其他语言编写的代码提供任何安全检查。程序员可能因为误用FFI反而导致更多安全问题,这就违背了使用Rust的初衷。目前已经有研究显示,FFI的误用已经成为导致Rust内存安全问题的重要原因之一。即便是使用纯Rust编写的软件也依然会受到影响,因为它可能依赖其他使用了FFI的Rust软件包。根据作者的统计,在目前的约77000个Rust官方软件包中,超过72%的软件包至少依赖一个包含了FFI的软件包(考虑多级依赖的情况)。
为了保护FFI的使用,Rust社区起草了很多文档用来指导用户正确地编写unsafe代码。同时提供了一些工具例如rust-bindgen和safer_ffi来自动地生成FFI。但这些只能帮助用户编写正确的接口和使用正确的数据类型,在跨语言场景下的堆内存管理仍然是一个问题。虽然跨语言场景在其他编程语言中也很常见,其安全问题也已经有许多研究,但Rust使用其特有的所有权系统(Ownership System)进行内存管理,导致了其独特的内存安全问题。
在这篇文章中,作者研究了Rust在跨语言场景下的堆内存管理问题,尤其是由Rust的所有权系统和C/C++的手动内存管理相结合导致的安全问题。同时作者实现了FFIChecker,一个基于静态分析的错误检测工具。

Rust的所有权机制和跨语言内存管理
Rust的所有权机制是它能在编译期消除内存安全问题的关键。所有权系统可以看作是一个自动内存管理系统,在所有权系统下,任意时刻每一个值都有且仅有一个所有者(owner)。当所有者离开它的作用域,这个值的内存将会被清理。当需要把一个值传递给程序的其他部分时,程序员可以选择:(1)拷贝/克隆(copy/clone)这个值,(2)移动(move)所有者,(3)借用(borrow)所有者。其中拷贝/克隆适用于简单的数据类型,例如整数类型。移动适用于复杂数据类型,尤其是内部使用了堆内存的类型,例如向量类型(vector)。一旦所有权被移动,根据所有者的唯一性,旧的所有者会立即失效。而借用可以暂时地通过引用(reference)访问一个值而不改变其所有者。
因为C/C++的手动内存管理是显然不安全的,在本文中,作者只考虑堆内存在Rust中申请,并通过FFI传递给C/C++的情况。综上所述,有两种方法来将Rust中的堆内存传递给FFI:(1)借用(borrow)堆对象,将它的引用传递给FFI;(2)移动(move)堆对象至FFI。对于借用,所有权依然保留在Rust一边,因此所有权系统仍然会负责这块堆内存的释放。对于移动,由于旧的所有者已失效,Rust的所有权系统将彻底“忘记”这块堆内存的存在,对它的管理完全被交给外部函数。

FFI造成的内存安全与内存管理错误
作者总结了跨语言场景下,Rust程序可能出现的内存安全与内存管理错误,分为以下三类:
  1. 内存损坏(Memory Corruption):由于堆内存被FFI传递到了外部函数,Rust无法保证其安全性,从而内存管理的责任又回到了程序员身上。这意味着C/C++中常见的内存安全错误(例如use-after-free,double-free,内存泄露等),理论上都有可能发生。
  2. 异常安全(Exception Safety):Rust不支持其他语言中常见的try-catch语法来处理异常。Rust所采用的方式是,对所有可恢复的错误(recoverable errors),编译器要求程序员必须处理这些错误,或者将错误返回给上层函数,由上层函数处理;对于所有不可恢复的错误(unrecoverable errors),程序将终止执行并做栈展开(stack unwinding)。所有栈中的对象的析构函数(destructors)将会被执行,以防止内存泄露。然而当需要将Rust中申请的内存传递给外部函数时,程序员往往需要暂时创建不健全的内存状态(例如使用未初始化的内存),当外部函数执行完后,程序员再去清理这个状态以消除不良影响。如果有异常在外部函数执行时产生,程序终止执行并做栈展开,从而导致后续的清理操作不会被执行。未被清理的不健全的内存状态得以长期存在并造成安全漏洞。
  3. 未定义行为(Undefined Behavior):C库经常提供函数来创建或销毁堆对象(通常使用malloc和free函数来实现)。为了使用这些已有的库,Rust开发者经常需要将这些C函数加以封装。一个常见的问题是,开发者往往错误地混合使用不同语言的内存申请/释放。例如在Rust中使用Box类型申请堆内存,但在C中使用free来释放它。这样混合使用不同语言的内存管理机制是一种未定义行为。因为(1)Rust和C/C++可以使用不同的内存分配器,例如在Linux下,Rust可以使用jemalloc,而C默认使用ptmalloc;(2)Rust在创建/释放对象时会运行其构造函数和析构函数,而C的内存管理没有类似的概念。

FFIChecker的设计
作者提出使用静态分析的方法来检测这些错误,并实现了工具FFIChecker,其大致工作流程如下图所示。
G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker
给定一个软件包(假设包含了Rust和C/C++代码),FFIChecker首先将Rust和C/C++代码分别编译成LLVM中间表示(LLVM IR),然后在LLVM IR上执行静态分析,跟踪每一块Rust中申请的堆内存的数据流转移和它的状态,即它是否被借用(borrow)或移动(move)。最后,如果一块堆内存被FFI传递给外部函数,FFIChecker继续分析这个外部函数,判断这块堆内存是否被外部函数释放。基于堆内存的状态,就可以判断它是否被正确地管理。例如如果堆内存被移动(move)至FFI,而且未被外部函数释放,那么这将会是一个内存泄露(Memory Leak)。再如一个堆内存被借用(borrow)至FFI,但在外部函数中被释放,那么这将会是一个Double-free或Use-after-free。

实验结果
为了评估FFIChecker的效果,作者从Rust官方包管理网站crates.io上抓取同时包含Rust和C/C++的软件包,最终收集到987个软件包(共包含了3, 232, 574行Rust和46, 321, 573行C/C++)。FFIChecker经过分析,产生了222个错误报告,经作者人工检查,确认了其中12个软件包中34个漏洞(19个内存泄露,3个异常安全错误,12个未定义行为)。性能方面,分析所有987个软件包共花费了5.2个小时,分析过程中最高内存占用为4.1GB。具体结果如下图所示。
G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker
源码:https://github.com/lizhuohua/rust-ffi-checker
论文下载:https://zhuohua.me/assets/ESORICS2022-FFIChecker.pdf


投稿作者介绍:
李卓华,目前是香港中文大学ANSR Lab五年级博士生。他的研究方向为系统安全和程序分析。
主页:https://zhuohua.me
ANSR Lab主页:
http://ansrlab.cse.cuhk.edu.hk/

原文始发于微信公众号(安全研究GoSSIP):G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker

版权声明:admin 发表于 2022年10月18日 下午7:27。
转载请注明:G.O.S.S.I.P 阅读推荐 2022-10-18 FFIChecker | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...