Rust的安全幻影:语言层面的约束及其局限性

一、Rust 简介 Rust语言自其发布以来就备受人们关注,作为一门现代的系统级编程语言,Rust在安全性方面引起了人们的极大兴趣。它与其他语言相比,引入了一系列创新的安全特性,旨在帮助开发者编写更可靠、更安全的软件。在这个基础上,许多大厂开始纷纷在自己的项目中引入Rust,比如Cloudflare的pingora,Rust版的git – gitxoide,连微软都提到要将自家的win32k模块用rust重写,足以见得其火爆程度。 之所以人们对Rust那么充满兴趣,除了其强大的语法规则之外,Rust提供了一系列的安全保障机制也让人非常感兴趣,其主要集中在以下几个方面: 内存安全:Rust通过使用所有权系统和检查器等机制,解决了内存安全问题。它在编译时进行严格的借用规则检查,确保不会出现数据竞争、空指针解引用和缓冲区溢出等常见的内存错误。 线程安全:Rust的并发模型使得编写线程安全的代码变得更加容易。它通过所有权和借用的机制,确保在编译时避免了数据竞争和并发问题,从而减少了运行时错误的潜在风险。 抽象层安全检测:Rust提供了强大的抽象能力,使得开发者能够编写更加安全和可维护的代码。通过诸如模式匹配、类型系统、trait和泛型等特性,Rust鼓励使用安全抽象来减少错误和提高代码的可读性。 Rust强大的编译器管会接管很多工作,从而尽可能的减少各种内存错误的诞生。 二、Rust 不会出现漏洞吗? 在 Rust 的各类机制下,开发人员在编译阶段被迫做了相当多的检查工作。同时在 Rust 的抽象机制下,整体的开发流程得到了规范,理论上应该是很难再出现漏洞了。然而,安全本质其实是人,错误本质上是由人们的错误认知引发的。即便是在 Rust 的保护之下,人们也是有可能犯错,从而导致新的问题的出现。对于这种场景,我们可以用一种宏观的方法论来概括,那就是认知偏差。这里可以用一个图来大致描述一下这个认知偏差: 换句话说,在使用Rust开发中,人们认为Rust能够提供的防护和Rust实际上提供的防护,这两者存在一定的差异。具体来说,可以有一下几种场景: Rust 检查时,能否防护过较为底层的操作状态? Rust 自身特性是否会引入问题? Rust 能否检查出作为mod 或者 API被其他人调用时,也能完全保护调用安全吗? 为了能够更好的了解认知差异,接下来我们就介绍几种比较典型的 Rust 下容易出现的漏洞。 三、漏洞案例一:对操作系统行为错误的认知 再进行开发过程中,Rust 通常会需要与操作系统底层进行交互。然而在这些操作过程中,本质上是对底层的API 或者对底层操作系统的操作,此时考察的是开发者对于操作系统的理解。而Rust编译器的防护机制并无法直接作用于这些底层的操作系统对象,从而会导致错误的发生。 一种常见的认知偏差就是默认操作系统提供的特性,比如说接下来要提到的特殊字符过滤规则。 3.1 BatBadBut(CVE-2024-24576) 在2024年4月,安全研究员RyotaK公开了一种他发现现有大部分高级语言中常见的漏洞类型,取名为BatBadBut,其含义为batch文件虽然糟糕,但不是最糟糕的。 batch files and bad, but not the worst 在Windows下,想要执行bat文件就必须要启动一个cmd.exe,所以执行的时候通常会变成cmd.exe /c test.bat。 每个高级语言在Windows平台下需要创建新的进程的时候,最终都会调用Windows的APICreateProcess。为了防止命令注入,它们大多数会对参数进行一定的限制,然而Windows平台下的CreateProcess存在一定的特殊行为,使得一些常见的过滤手段依然能够被绕过。作者给了一个nodejs的例子,在nodejs中,当进行进程创建的时候,通常是这样做的 const { spawn } = require('child_process'); const child = spawn('echo', ['hello', 'world']); 这种做法通常是没问题的,此时由CreateProcess创建的进程为echo,参数为后续的两个参数。同时,这个调用过程中伴随的如下的过滤函数,会将"过滤成\" /* * Quotes command line arguments * Returns a pointer to the end (next char to be written) of the buffer */ WCHAR* quote_cmd_arg(const WCHAR *source, WCHAR *target) { /* * Expected input/output: * input : hello"world * output: "hello\"world" * output: "hello world\\" */ } 此时,上述的指令会形成如下的指令:...

2024年10月30日 · 7 分钟 · l1nk

Rust逆向入门:从反编译视角学习内存模型

一、前言 Rust反编译一直是比较困难的问题,Rust强调零成本抽象,在使用高级特性(如泛型、闭包、迭代器等)时为了不引入额外的运行时开销,编译器会生成高度优化且复杂的机器码,从而难以直接恢复高层的抽象结构,除此之外,Rust 的所有权系统及其借用检查器在编译过程中被彻底消解成运行时代码。这个过程生成了许多低级的内存管理代码,这些代码在反编译时难以重新构建成高层次的所有权和生命周期语义。尽管反编译 Rust 代码有诸多困难,但进行 Rust 反编译依然具备现实意义,例如分析闭源Rust代码,寻找潜在的安全漏洞。本文旨在学习Rust基本内存数据结构及其内存布局,并且通过反编译视角理解Rust编译器到底做了什么事。 值得注意的时,Rust目前没有确定内存模型,因此本文谈到的是Rustc编译器实现的Rust模型。 Rust 目前没有确定内存模型 Rust does not yet have a defined memory model. Various academics and industry professionals are working on various proposals, but for now, this is an under-defined place in the language. 二、反编译器准备 本文中的很多例子会使用到反编译工具,例如IDA或者ghidra。ghidra在11版本以后增加了对Rust支持,在使用ghidra进行反编译Rust工具时只需要选中Demangler Rust即可。 IDA则需要额外的IDARustDemangler插件,不管是ghidra的Demangler Rust功能亦或是IDARustDemangler插件,其功能都是将Rust二进制文件中经过编译器mangle过的符号进行demangle,得到原始符号,以下以IDA得视角对比了demangle之前与demangle之后得代码视图,可以看到可读性大大增加。 demangle之前: demangle之后: 三、函数调用__Rustcall 在x86_64平台UNIX系统下面,Rust遵循System V ABI,即传参会通过rdi、rsi、rdx、rcx、r8、r9等,返回值会通过rax,在某些情况下,compiler会进行返回值优化(Return Value Optimization,RVO),这时候函数调用约定就会发生变化,返回值不再是使用rax进行传递,而是使用rdi(函数的第一个参数)。以如下例子为例,我们查看ghidra编译器到底做了什么? fn add_str_ret_str(a:&str,b:&str)->String{ return a.to_string()+&b.to_string(); } 对于上述代码,因为在返回时新建了对象(没有新建对象不会触发RVO,此时依旧通过rax传递对象),会触发返回值优化,使得返回值通过第一个参数进行传递。 IDA视角下得Rustcall,IDA会将第一个参数命名为retstr,提醒用户这个字段是返回值。 在ghidra视角下rust调用使用__rustcall关键字标识,ghidra使用return_storage_ptr来标记返回值。 四、Rust内存布局 在Rust中基本类型、引用(存储的是变量的地址,大小为8字节)、数组(连续内存块)与传统的C、C++内存布局一样,因此本文不再赘述。本文主要探究Rust特有实现,例如动态数组、String、动态大小类型(Dynamic Sized Type,DST)。 4.1 动态数组与String Rust中的动态数组Vec以及String类型的底层实现与C++容器相同,其采用三个部分来表示,分别是: pointer:指向数据字节流buffer中存储的数据; length:buffer中字节流的字节长度; capacity:buffer的长度。 实际上看String的实现,会发现String的实现基于Vec,以下代码摘自Rust底层实现:...

2024年07月17日 · 3 分钟 · b4tm4n