RUST编程语言利用缓解

2020-09-24 19:36:51

Rust编程语言通过拥有[3]、引用和借用[4]以及slicetypes[5]特性来提供内存[1]和线程[2]的安全保证。然而,“不安全的锈”[6]引入了不安全的块、不安全的函数和方法、不安全的特性以及不受借用规则约束的新类型。

Rust标准库的部分实现为不安全代码之上的安全抽象(历史上容易受到内存损坏的影响[7])。此外,Rust代码和文档鼓励在不安全代码上创建安全抽象。这可能会造成错误的安全感,因为未正确审查和测试安全代码。

不安全锈会禁用某些提供内存和线程安全保证的功能。这会导致程序或库容易出现内存损坏(CWE-119)[8]和并发问题(CWE-557)[9]。现代的Cand C++编译器提供了利用缓解措施,以增加利用这些问题导致的漏洞的难度。因此,Rust编译器还必须支持这些利用漏洞的缓解措施,以减轻由于使用不安全的锈蚀而导致的漏洞。这篇文章将记录这些利用漏洞的缓解措施,以及它们如何应用于铁锈。

本节记录了在AMD64体系结构和等效体系结构上为Linux操作系统构建程序时,适用于Rust编译器的漏洞利用缓解措施。1本节中的所有示例都是使用Debian测试(Bullseye)上的Rust编译器版本1.40.0(2019-12-19)构建的。

Rust编程语言目前没有规范。Rust编译器(即,rustc)是语言参考实现。本文中所有提到的“Rust编译器”都指的是LanguagereReference实现。

位置无关的可执行文件通过为可执行文件生成位置无关的代码,并指示动态链接器以类似于随机加载地址的共享对象的方式加载它,从而也受益于地址空间分层随机化(ASLR),从而增加了使用代码重用开发技术(例如,面向返回的编程(ROP)和变体)的难度。这也被称为“全ASLR”。

Rust编译器支持位置无关的可执行文件,并且从版本0.12.0(2014-10-09)[10]-[13]开始默认启用它。

对象类型为et_dyn(即共享对象)而不是et_exec(即可执行文件)的可执行文件是独立于位置的可执行文件(参见图1)。

整数溢出检查通过检查无法以其类型表示的有符号和无符号整数计算的结果,从而防止程序发生未定义和意外的行为(这可能会导致漏洞),从而导致溢出或回绕。

Rust编译器支持整数溢出检查,并在1.0.0(2015-05-15)[14]-[17]版本开始启用调试断言时启用它,但直到1.1.0(2015-06-25)[16]版本才完成对它的支持。控制整数溢出检查的选项后来稳定在版本1.17.0(2017-04-27)[18]-[20]中。

$Cargo运行编译hello-rust-Integral v0.1.0(/home/rcvalle/hello-rust-integer)在0.23s内完成dev[unOptimized+debuginfo]目标,运行`target/debug/hello-rust-Integrger`thread';main';在';尝试使用overflow';,src/main.rs:3:23运行时死机。注意:使用`RUST_Backtrace=1`环境变量运行可显示回溯跟踪。

$Cargo运行--释放编译hello-rust-integer v0.1.0(/home/rcvalle/hello-rust-teger)运行`target/release/hello-rust-Integrger`u:0,在0.23秒内完成[优化]目标版本。

启用调试断言时会启用整数溢出检查(参见图。3),并且当调试断言被禁用时禁用(参见图4)。要独立启用整数溢出检查,请使用选项控制整数溢出检查、作用域属性或显式检查方法(如CHECKED_ADD 2)。

建议在打算包装语义时使用显式包装方法(如WRAPING_ADD),并在使用不安全锈时始终使用显式检查和包装方法。

2.检查方法、溢出方法、饱和方法、包装方法详见https://doc.rust-lang.org/std/primitive.u32.html(以u32为例)。↩。

不可执行的内存区限制了可用于执行任意代码的内存区,从而增加了利用该漏洞的难度。大多数现代处理器支持操作系统将内存区域标记为不可执行,但以前在不提供支持的处理器上由软件仿真,如Grsecurity/Pax PAGEEXEC和SEGMEXEC。这也称为“不执行(NX)位”、“执行禁用(XD)位”、“从不执行(XN)位”等。

Rust编译器支持不可执行的内存区域,并通过自初始版本0.1(2012-01-20)[21]、[22]开始默认启用它,但此后版本[23]-[25]出现倒退,并且从版本1.8.0(2016-04-14)[25]起默认强制执行。

$readelf-l target/release/hello-rust|grep-A 1 GNU_STACK GNU_STACK 0x00000000000000 0x00000000000000 0x0000000000000000 0x0000000000000000 0x00000000000000 0x00000000000000 rw 0x10。

在PF_X(即,可执行)标志未设置的程序标题表中存在PT_GNU_STACK类型的元素表示对于给定的二进制文件启用了非可执行存储器区域3(见图5)。相反,在设置了PF_X标志的程序标题表中存在PT_GNU_STACK类型的元素或者在程序标题表中不存在PT_GNU_STACK类型的元素表示对于给定的二进制文件没有启用不可执行的存储器区域。

3.有关它为什么会影响堆栈以外的其他内存区域的更多信息,请参阅附录部分。↩。

堆栈冲突保护通过在堆栈增长时从堆栈页读取以在尝试从保护页/区域读取时导致页缺省,从而保护堆栈不与另一个内存区域重叠-允许使用彼此覆盖两者中的任意数据。这也称为“堆栈探测”或“堆栈探测”。

Rust编译器通过堆栈探测支持堆栈冲突保护,从1.20.0(2017-08-31)[26]-[29]版本开始默认启用。

Fn hello(){println!(";Hello,world!);}fn main(){let_:[U64;1024]=[0;1024];hello();}。

要检查是否为给定的二进制文件启用了堆栈冲突保护,请搜索对__rust_probestack的交叉引用。__rust_probestack在其堆栈大小大于页面大小的函数的序言中被调用(参见图6),并且可以通过修改如图7和图8所示的hello-rust示例来强制用于说明目的。

只读位置调整通过将包含位置调整和位置调整信息(即.init_array、.fini_array、.dynamic和.get)的段标记为只读,可防止这些段被覆盖。这也被称为“部分RELRO”。

自版本1.21.0(2017-10-12)[30]、[31]起,Rust编译器支持只读位置调整,并默认启用。

在程序标题表中出现PT_GNU_RELRO类型的元素表示为给定的二进制文件启用了只读位置调整(参见图。9)。相反,如果程序头表中没有PT_GNU_RELRO类型的元素,则表示没有为已激活的二进制文件启用只读位置调整。

立即绑定通过指示动态链接器在启动期间将控制权转移到程序之前执行所有重定位,从而保护包含重定位的其他段(即,.go.plt)不被覆盖,以便所有包含重定位的段都可以标记为只读(当与只读重定位结合使用时)。这也称为“fullRELRO”。

Rust编译器支持立即绑定,从版本1.21.0(2017-10-12)[30]、[31]开始默认启用。

在动态部分中出现具有DT_BIND_NOW标签和DF_BIND_NOWflag 4的元素表示对于给定的二进制文件启用了立即绑定(参见图10)。相反,DYNAMIC部分中缺少带有DT_BIND_NOW标记和DF_BIND_NOW标志的元素表示没有为给定的二进制文件启用立即绑定。

在程序标题表中存在PT_GNU_RELRO类型的元素,并且在动态部分中存在具有DT_BIND_NOW标签和DF_BIND_NOW标志的元素,表明对于给定的二进制文件启用了完全RELRO(参见图9和图10)。

堆损坏保护通过执行几个检查来保护动态分配的内存,例如检查列表元素之间的损坏链接、无效指针、无效大小、分配的相同内存的两次/多次“释放”,以及这些情况的许多角落情况。这些检查是特定于实现的,并且根据分配器的不同而有所不同。

较新的基于ARM的处理器将通过标记内存分配并在每次内存访问时自动检查是否使用了正确的标记(即ARM内存标记扩展(MTE)),为检测违反内存安全的行为提供硬件帮助。

Rust的默认分配器历史上一直是jemalloc,它长期以来一直是问题的起因,也是很多讨论的主题[32]-[38]。因此,从版本1.32.0(2019-01-17)[39]开始,它作为默认分配器被删除,取而代之的是操作系统的标准C库默认分配器5。

Fn main(){设mut x=Box::New([0;1024]);for i in 0..。1026{不安全{let elem=x.get_uncheck_mut(I);*elem=0x41414141414141u64;}。

$Cargo运行编译hello-rust-heap v0.1.0(/home/rcvalle/hello-rust-heap)在运行`target/debug/hello-rust-heap`free():下一个大小(正常)无效中止的0.25秒内完成了dev[未优化+debuginfo]目标。

$Cargo运行--释放编译hello-rust-heap v0.1.0(/home/rcvalle/hello-rust-heap)在0.25秒内完成发布[优化]目标,运行`target/release/hello-rust-heap`free():无效的下一个大小(正常)已中止。

在使用默认分配器(即GNU分配器)时执行堆损坏检查,如图12和图13所示。

5.Linux的标准C库缺省分配器是GNU Allocator,它从Wolfram Gloger的ptmalloc(Pthread Malloc)派生而来,在Turnis中由Doug Lea的dlmalloc(Doug Lea Malloc)派生而来。↩。

堆栈崩溃保护通过在局部变量和保存的返回指令指针之间插入随机保护值,并在从函数返回时检查此值是否已更改,从而保护程序免受基于堆栈的缓冲区溢出的影响。这也称为“堆栈保护器”或“堆栈粉碎保护器(SSP)”。

Rust编译器不支持堆栈粉碎保护。但是,存在更全面的堆栈粉碎保护替代方案,如asshadow和安全堆栈(请参阅后端控制流保护)。

要检查是否为给定的二进制文件启用了堆栈粉碎保护,请搜索对__STACK_CHK_FAIL的交叉引用。Hello-rust中对__STACK_CHK_FAIL的唯一交叉引用来自静态链接的libbacktracelilibrary(参见图14)。

前沿控制流保护通过执行检查来确保间接分支的目的地是它们在控制流图中的有效目的地之一,从而保护程序免受其控制流的改变/劫持。这些检查的全面性因实现而异。这也称为“前沿控制流完整性(CFI)”。

作为英特尔控制流实施技术(CET)的一部分,较新的处理器为前沿控制流保护提供硬件辅助,例如ARM分支目标识别(BTI)、ARM PointerAuthentication和英特尔间接分支跟踪(IBT)。但是,基于ARM BTI和英特尔IBT的实施不如基于软件的实施(如LLVM ControlFlowIntegrity(CFI)和市售的GrSecurity/Pax Reuse Attack Protector(RAP))全面。

Rust编译器不支持Linux 6上的前沿控制流保护。目前正在进行添加对杀菌器[40]的支持的工作,这可能包括也可能不包括对LLVM CFI的支持。

__CFI_INIT符号(以及对__CFI_CHECK的引用)的出现表明LLVM CFI(即,前缘控制流保护)已为给定的二进制文件启用。相反,如果没有__CFI_initSymbol(以及对__CFI_CHECK的引用),则表示LLVM CFI对于给定的二进制没有启用(参见图15)。

影子堆栈通过将保存的返回指令指针的副本存储在单独的(影子)堆栈上,并在从函数返回时将这些副本用作权威值,从而防止保存的返回指令指针被覆盖。这也称为“ShadowCallStack”和“Return Flow Guard”,被认为是后端控制流保护(或“后端CFI”)的实现。

安全堆栈不仅保护保存的返回指令指针,而且通过将不安全变量(如大型数组)存储在单独的(不安全)堆栈上,并在单独的堆栈上使用这些不安全变量,从而保护寄存器溢出和某些局部变量不被覆盖。它也称为“安全堆栈”,也被认为是后端控制流保护的一种实现。

影子堆栈和安全堆栈都旨在成为堆栈粉碎保护的更全面的替代方案,因为它们保护保存的返回结构指针(以及安全堆栈情况下的其他数据)不受二进制写入和非线性越界写入的影响。

较新的处理器为后端控制流保护提供硬件帮助,例如作为英特尔CET一部分的ARM指针身份验证和英特尔影子堆栈。

Rust编译器不支持卷影堆栈或安全堆栈。目前正在进行增加对消毒剂[40]的支持的工作,这可能包括也可能不包括对安全堆栈7的支持。

出现__safestack_init符号表示已为给定的二进制文件启用了LLVM SafeStack。相反,如果没有__safestack_init符号,则表示LLVM安全堆栈未针对已启用的二进制文件启用(参见图16)。

7.由于性能和安全问题,删除了在LLVM中相当于AMD64体系结构的影子堆栈实现。↩。

在Linux Standard Base(LSB)CoreSpecification的最新版本中,PT_GNU_STACK程序标头指示堆栈是否应该是可执行的,如果没有这个标头,则表明堆栈应该是可执行的。但是,Linux内核当前在加载任何设置了PT_GNU_STACK程序头和PF_X标志的可执行文件时或在没有该头文件的情况下设置READ_IMSIONS_EXEC属性,导致不仅堆栈而且所有可读虚拟内存映射都是可执行的。

2012年(由本文作者)进行了一次修复该问题的尝试,2020年进行了另一次尝试。前者从未登陆,后者部分修复了该问题,但引入了其他问题-缺少PT_GNU_STACK程序头仍然导致堆栈以及所有可读虚拟内存映射在某些体系结构(如IA-32和等效体系结构)中可执行(或者导致堆栈在某些体系结构中不可执行,如AMD64和等效,这与LSB相冲突)。

READ_INSERSION_EXEC属性需要通过单独的选项与PT_GNU_STACK程序头完全分开(只要需要READ_IMSIVES_EXEC,就可以使用orsetarch-X),而缺少PT_GNU_STACK程序头需要有更安全的默认值(与READ_IMSIGNES_EXEC无关)。

S·大卫杜夫(S.Davidoff)。“多年来,铁锈的标准图书馆是多么容易受到攻击,却没有人注意到。”5~6成熟。Https://medium.com/@shnatsel/how-rusts-standard-library-was-vulnerable-for-years-and-nobody-noticed-aebf0503c3d6.。

“内存缓冲区界限内的操作限制不正确(CWE-119)。”米特雷·CWE名单。Https://cwe.mitre.org/data/definitions/119.html.。

D·迈凯。“在LINUX上为Full ASLR#16340默认启用PIE。”GitHub。Https://github.com/rust-lang/rust/pull/16340.。

H·威尔逊。“关于铁锈中整数溢出的神话和传说。”Huon在互联网上。Http://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/.。

A·塞普(A.Seipp)。“确保Librustrt.so与不可执行堆栈链接。#1066。”GitHub。Https://github.com/rust-lang/rust/pull/1066.。

D·迈凯。“锈二进制文件不应该有可执行堆栈#5643。”GitHub。Https://github.com/rust-lang/rust/issues/5643.。

A·克拉克。“显式禁用linux和bsd#30859上的堆栈执行。”gihub。Https://github.com/rust-lang/rust/pull/30859.。

B·斯特里格尔。将堆栈探测支持扩展到非第1层平台,并阐明缓解依赖于LLVM的不安全因素的策略#43241。GitHub。Https://github.com/rust-lang/rust/issues/43241.

A.克莱顿。更改全局默认分配器(RFC1974)#27389的跟踪问题。GitHub。Https://github.com/rust-lang/rust/issues/27389.。

B.安德森。“将默认全局分配器切换到SYSTEM,removealloc_jemalloc,使用rustc#36963中的jemallocator。”GitHub。Https://github.com/rust-lang/rust/issues/36963.