Rust 乱炖

围绕 Rust 随便写写,同步公众号 「觉·学社」。

2021 年 05 月

Rust 与 安全 | Rust 让恶意软件也变强了

作者:张汉东 / 2021-05-05


技术 是一把锋利的双刃剑。 —— 鲁迅。

背景

今天看到 proofpoint 发表了一篇题为Buer Loader 用 Rust 创造了新的变种 (New Variant of Buer Loader Written in Rust)的文章。

Proofpoint是一家位于加利福尼亚州桑尼维尔的企业安全公司,为入站电子邮件安全,出站数据丢失防护,社交媒体,移动设备,数字风险,电子邮件加密,电子发现和电子邮件归档提供软件即服务和产品。

Buer 是在地下市场上出售的下载程序,以分发包括勒索软件在内的其他恶意软件。 Proofpoint于 2019年 首次观察到 Buer。

Proofpoint研究人员确定了4月初通过伪装成(快递公司 DHL)发货通知的电子邮件分发的 Buer 恶意软件加载程序的新变种。 邮件中包含指向恶意 Microsoft Word 或 Excel 文档下载的链接,这些链接使用宏来投放新的恶意软件变体。

Proofpoint 把新的变种称为 RustyBuer,因为它们用 Rust的语言完全重写,这与以前的C编程语言有所不同。常见的恶意软件以完全不同的方式编写是很少见的。用 Rust 重写恶意软件可以使威胁者更好地逃避现有的 Buer 检测功能,并且通过使用不同的诱饵技术,提升链接的点击率。这一切都表明威胁者正在以多种方式发展技术,从而逃避检测并试图提高成功的点击率。

RustyBuer 直接嵌入到文档宏中,需要用户交互才能启动感染。此宏利用应用程序绕过(通过 LOLBAS 的 Windows Shell DLL)逃避了端点安全性机制的检测。

尚不清楚威胁者为何花时间和精力用新的编程语言重写恶意软件,但是Proofpoint研究人员指出了两个可能的原因:

  • Rust 是一种越来越流行的编程语言,它比 C 语言更高效且具有更广泛的功能集。(例如,Microsoft越来越多地在其产品中使用它,并于2021年2月加入了Rust 基金会)。

  • 用 Rust 重写恶意软件可以使威胁参与者逃避基于 C 语言编写的恶意软件功能的现有 Buer 检测。恶意软件作者对其进行了编程,使其应与现有 Buer 后端 C2 服务器和面板保持兼容。

乱炖

其实用 Rust 写恶意软件,这个案例并不是第一个。早在 2016 年 Dr.Web(俄罗斯杀毒软件公司 大蜘蛛)公司就发现了一款用 Rust 实现的 针对 Linux 的特洛伊木马 Linux.BackDoor.Irc.16。Doctor Web的分析师认为,Linux.BackDoor.Irc.16 实际上是一个原型(概念验证),因为它无法自我复制。然而 RustyBuer 就是一款完全黑产化的软件了。

随着目前 Rust 知名度越来越高,Rust 在安全/黑产领域可能会越来越流行。抛开黑产软件的伦理道德不谈,单从技术以及 Rust 自身而言,在这种「攻与防」的觥筹交错中,也许会促进各种针对 Rust 的检测工具诞生,进一步丰富生态,促进 Rust 发展。

因此,我对目前安全领域生态中有多少用 Rust 实现的工具产生了好奇。接下来盘点一下 GitHub/GitLab 上发现的一些用 Rust 实现的安全工具。欢迎补充。

渗透测试工具 Metasploit (无)Rust 重写计划

2017 年有人在 Metasploit issue 留言,请求 Metasploit 用 Rust 重写。当然,这个 issue 被无情关闭了。因为 Metasploit 当年是从 Python 转为 Ruby 重写,是看中了 Ruby 强大的 DSL 能力。作为渗透测试工具的扛把子,Metasploit 目前没有使用 Rust 重写的理由。除非,有人直接提交 PR,用 Rust 完成了大部分功能,不过我想这件事不会发生。

不过还有个曲线救国的办法,就是用 Rust 重写 Ruby。这个还是有希望的:artichoke/artichoke。Artichoke 就是用 Rust 实现 Ruby 的项目,目前维护也很活跃,已经兼容 MIR(Ruby 2.6.3) 规格。

Metasploit 还有一个功能,叫「Metasploit's Hardware Bridge」,提供了一系列Hardware REST API,可以让 Metasploit 拥有对硬件(IoT领域)操作的能力。对于 REST Api,用任何语言都可以包装它,Rust 也可以。不过这个相关文档有五年没有更新了,不知道发展如何。

扫描

rustscan/rustscan

RustScan 是一个现代化的端口扫描器,并且提供脚本引擎来支持运行 Python,Lua,Shell。

该工具的特色是:

  • 高性能。3 秒内可以扫描 65,000 个端口。
  • 全面的脚本引擎支持。自动将结果传送到 Nmap,并支持自定义脚本。
  • 支持 自适应学习(Adaptive learning)。用的越多,RustScan 越智能,不是基于臃肿的机器学习,主要是基于基本数学。

feroxbuster

一款高性能的内容发现工具,用于渗透测试。feroxbuster 使用蛮力结合单词列表​​在目标目录中搜索未链接的内容,这是一种攻击行为。这些资源可能存储有关 Web 应用程序和操作系统的敏感信息,例如源代码,凭据,内部网络寻址等。此攻击也称为可预测资源位置,文件枚举,目录枚举和资源枚举。

隐身匿名

Tor 和 Rust

zero-gear/rusty-tor

一个概念验证的 Tor 客户端。不过有两年没有维护了。

免杀

Rust 写 Shell Code 也是可以的。

重点介绍:

amber。号称首款 反射式 PE 打包器,目前开发很活跃。

Amber是位置无关(反射)的 PE 加载器,可在内存中执行本机PE文件(EXE,DLL,SYS ...)。 它实现了秘密的的内存有效负载部署,可用于绕过防病毒,防火墙,IDS,IPS产品和应用程序白名单缓解措施。 由Amber生成的反射性有效负载可以从远程服务器上演,也可以像在通用Shellcode中一样直接在内存中执行。

amber 提供的打包方法,可以改变将恶意软件传送到系统的方式。 通过尝试通过无文件代码注入将有效负载传递到系统,可以直接绕过大多数安全机制,而不是尝试寻找提供机器学习数据集的新的反检测技术。 使用这种新的打包方法,可以将已编译的PE文件转换为可与常见软件漏洞(例如缓冲区溢出)一起使用的多阶段感染有效负载。

更多介绍:Introducing New Packing Method: First Reflective PE Packer Amber

Exploits/ payloads / hacking

这个开发者 kpcyrd,编写了一系列安全工具。

  • sn0int, 半自动高级公开资源情报(OSINT)框架和程序包管理器,用于扫描目标IP地址、电子邮件、网站和组织信息,并从不同消息源收集各种情报信息。

  • sniffglue,多线程网络嗅探器。Kpcyrd 经常使用tcpdump,但他认为输出更友好的话会更好,并且 wireshark 也经常存在漏洞,tcpdump 有时也有漏洞,所以他用 Rust 实现了这个工具。

  • badtouch, 可编写脚本的网络身份验证破解程序。

  • rshijack,用 Rust 重写了 shijack,实现 TCP 会话劫持。

Fuzz 工具

其他工具

Rust生态安全漏洞总结系列 | Part 2

本系列主要是分析RustSecurity 安全数据库库中记录的Rust生态社区中发现的安全问题,从中总结一些教训,学习Rust安全编程的经验。

本期分析了下面六个安全问题:

  • RUSTSEC-2021-0067 : Cranelift 模块中代码生成缺陷导致可能的 WASM 沙箱逃逸
  • RUSTSEC-2021-0054:rkyv crate 可能包含未初始化的内存
  • RUSTSEC-2021-0041:parse_duration 通过用太大的指数解析 Payload 来拒绝服务(DOS)
  • RUSTSEC-2021-0053: 算法库中 merge_sort::merge() 导致实现 Drop 的类型 双重释放( double-free)
  • RUSTSEC-2021-0068: iced x86 版本中 不合理(Soundness) 的问题
  • RUSTSEC-2021-0037: Diesel 库的 Sqlite 后端 UAF(use-after-free) bug

看是否能给我们一些启示。

RUSTSEC-2021-0067 : Cranelift 模块中代码生成缺陷导致可能的 WASM 沙箱逃逸

在 Cranelift 中发现了一个漏洞。具有未知输入的操作导致特权升级漏洞。 CWe正在将问题分类为CWE-264。 这将对机密性,完整性和可用性产生影响。

漏洞描述:

Cranelift X64后端的0.73.0中有一个错误,可以创建一个可能导致 Webassembly 模块中的潜在沙箱逃逸(sandbox escape )的场景。 版本0.73.0的Cranelift的用户应升级到0.73.10.74,以修复此漏洞。

如果未使用旧的默认后端,则在0.73.0之前的 Cranelift 用户应该更新为0.73.10.74

漏洞分析

此问题是在 Cranelift 新后端中引入的(Cranelift 经历过大的重构)。

一些背景: 寄存器分配

如果物理寄存器的数量不足以满足虚拟寄存器的需求,有些虚拟寄存器显然就只能映射到内存。这些虚拟寄存器称为溢出(spill)虚拟寄存器。寄存器分配算法的好坏直接决定了程序中寄存器的利用率。

Cranelift 寄存器分配相关文章:https://cfallin.org/blog/2021/03/15/cranelift-isel-3/

该文章还详细介绍了该团队如何保证 Cranelift 生成正确的代码。即便如此,还是产生了逻辑 Bug。

这个 Bug 是一个逻辑 Bug:

原因是,寄存器分配器重新加载比 64位 窄的溢出(spill)整数值时,从栈上加载的值执行了符号扩展而不是零扩展。

这对另一个优化产生了糟糕的影响:当我们知道产生32位值的指令实际上将其目标寄存器的高32位置零时,指令选择器将选择一个32到64位的零扩展运算符。因此,我们依赖于这些归零位,但值的类型仍然是I32,并且溢出/重新加载将这些比特位重构为I32的MSB的符号扩展。

所以,在某些特定情况下,如果i32值为指针,则可能会出现沙箱逃逸的情况。为堆访问发出的常规代码对 WebAssembly 堆地址进行零扩展,将其添加到64位堆基,然后访问结果地址。如果零扩展成为符号扩展,则模块可以在堆开始之前向后访问并访问最大2GiB的内存。

符号扩充 (sign-extend): 指在保留数字的符号(正负性)及数值的情况下,增加二进制数字位数的操作。

零扩充(zero-extend):用于将无符号数字移动至较大的字段中,同时保留其数值。

该 Bug 的影响力依赖于堆的实现。具体而言:

如果堆有边界检查。并且,不完全依赖于保护页面。并且堆绑定为2GiB或更小。则该 Bug 无法用于从另一个 WebAssembly 模块堆访问内存。

如果使用此 Bug 可访问的范围中没有映射内存,例如,如果 WebAssembly 模块堆之前有 2 GiB 保护区域,则可以减轻此漏洞的影响。

RUSTSEC-2021-0054:rkyv crate 可能包含未初始化的内存

漏洞描述:

rkyv是一个序列化框架 在序列化期间,可能无法初始化结构填充字节和未使用的枚举字节。 这些字节可以写入磁盘或发送不安全的通道。

漏洞分析

补丁代码:https://github.com/djkoloski/rkyv/commit/9c65ae9c2c67dd949b5c3aba9b8eba6da802ab7e

有问题的代码:


#![allow(unused)]
fn main() {
unsafe fn resolve_aligned<T: Archive + ?Sized>(
        &mut self,
        value: &T,
        resolver: T::Resolver,
    ) -> Result<usize, Self::Error> {
    // ...
    let mut resolved = mem::MaybeUninit::zeroed();
    // ...
}
}

mem::MaybeUninit::zeroed()函数会创建一个新的MaybeUninit<T>实例,并且该内存位会被填充0。但是这依赖于 T是否能被正确初始化。比如:MaybeUninit<usize>::zeroed()是初始化,但是MaybeUninit<&'static i32>::zeroed()就没有被正确初始化。这是因为 Rust 里引用不能为空。

所以,现在这个 resolver 是个泛型 T,不一定能正确初始化,所以有未初始化的风险。

修复之后的代码:


#![allow(unused)]
fn main() {
    let mut resolved = mem::MaybeUninit::<T::Archived>::uninit();
    resolved.as_mut_ptr().write_bytes(0, 1);
}

直接假设其没有正确初始化,然后使用write_bytes手工将其初始化,确保正确。

RUSTSEC-2021-0041:parse_duration 通过用太大的指数解析 Payload 来拒绝服务(DOS)

漏洞描述:

漏洞解析

parse_duration 库用来将字符串解析为持续时间(duration)。

问题代码:


#![allow(unused)]
fn main() {
if exp < 0 {
    boosted_int /= pow(BigInt::from(10), exp.wrapping_abs() as usize);
} else {
    boosted_int *= pow(BigInt::from(10), exp.wrapping_abs() as usize);
}
duration.nanoseconds += boosted_int;
}

此为 parse 函数内的代码片段,允许使用指数级的持续时间字符串解析,其中BigInt 类型与 pow 功能一起用于这类 Payload。该功能会导致长时间占用CPU和内存。

这允许攻击者使用 parse 功能来制造 DOS 攻击。虽然该库已经不维护了,而且star数也不多,但是不清楚依赖它的库有多少,可以使用 cargo-audit 来检查你项目里的依赖。

RUSTSEC-2021-0053: 算法库中 merge_sort::merge() 导致实现 Drop 的类型 双重释放( double-free)

漏洞分析

algorithmica是 Rust 实现算法的教学库,网站为:https://www.fifthtry.com/abrar/rust-algorithms/

该库中的归并排序的实现中,merge 函数导致 对列表元素持有双份所有权,所以会双重释放(double free)。

注意下面源码中,为 unsafe rust 实现。


#![allow(unused)]
fn main() {
 fn merge<T: Debug, F>(list: &mut [T], start: usize, mid: usize, end: usize, compare: &F) 
 where 
     F: Fn(&T, &T) -> bool, 
 { 
     let mut left = Vec::with_capacity(mid - start + 1); 
     let mut right = Vec::with_capacity(end - mid); 
     unsafe { 
         let mut start = start; 
         while start <= mid { 
             left.push(get_by_index(list, start as isize).read()); 
             start += 1; 
         } 
         while start <= end { 
             right.push(get_by_index(list, start as isize).read()); 
             start += 1; 
         } 
     } 
  
     let mut left_index = 0; 
     let mut right_index = 0; 
     let mut k = start; 
  
     unsafe { 
         while left_index < left.len() && right_index < right.len() { 
             if compare(&left[left_index], &right[right_index]) { 
                 
                 // 通过 `list[k] = ` 这种方式重复持有元素所有权
                 list[k] = get_by_index(&left, left_index as isize).read(); 
                 
                 left_index += 1; 
             } else { 
                 list[k] = get_by_index(&right, right_index as isize).read(); 
                 right_index += 1; 
             } 
             k += 1; 
         } 
  
         while left_index < left.len() { 
             list[k] = get_by_index(&left, left_index as isize).read(); 
             left_index += 1; 
             k += 1; 
         } 
  
         while right_index < right.len() { 
             list[k] = get_by_index(&right, right_index as isize).read(); 
             right_index += 1; 
             k += 1; 
         } 
     } 
 } 

unsafe fn get_by_index<T>(list: &[T], index: isize) -> *const T {
    let list_offset = list.as_ptr();
    list_offset.offset(index)
}
}

Bug 复现:

#![forbid(unsafe_code)]
use algorithmica::sort::merge_sort::sort;

fn main() {
    let mut arr = vec![
        String::from("Hello"),
        String::from("World"),
        String::from("Rust"),
    ];

    // Calling `merge_sort::sort` on an array of `T: Drop` triggers double drop
    algorithmica::sort::merge_sort::sort(&mut arr);
    dbg!(arr);
}

输出:


#![allow(unused)]
fn main() {
free(): double free detected in tcache 2

Terminated with signal 6 (SIGABRT)
}

该 Bug 还未得到修复。

此问题给我们的启示:不要为了刷题而忽略安全。

RUSTSEC-2021-0068: iced x86 版本中 不合理(Soundness) 的问题

漏洞描述:

漏洞分析

iced 用户在使用 miri 编译其项目时,发现 UB:


#![allow(unused)]
fn main() {
error: Undefined Behavior: memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3
    --> C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365:18
     |
365  |         unsafe { &*index.get_unchecked(self) }
     |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
             
     = note: inside `core::slice::<impl [u8]>::get_unchecked::<usize>` at C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365:18
     = note: inside `iced_x86::Decoder::new` at C:\Users\lander\.cargo\registry\src\github.com-1ecc6299db9ec823\iced-x86-1.9.1\src\decoder\mod.rs:457:42
note: inside `Emulator::run` at src\lib.rs:563:27
    --> src\lib.rs:563:27
     |
563  |         let mut decoder = Decoder::new(self.bitness, bytes, self.decoder_options);
}

该用户在使用 Decoder::new 的时候出现了 UB。在 iced相关源码中,即 iced/src/rust/iced-x86/src/decoder.rs 中,存在


#![allow(unused)]
fn main() {
let data_ptr_end: *const u8 = unsafe { 
    data.get_unchecked(data.len()) 
}; 
}

根据标准库文档描述:

Calling this method with an out-of-bounds index is undefined behavior even if the resulting reference is not used.

使用 界外索引调用该方法就是 未定义行为(UB),即便这个结果的引用没有被使用。

示例:


#![allow(unused)]
fn main() {
let x = &[1, 2, 4];

unsafe {
    assert_eq!(x.get_unchecked(1), &2);
    assert_eq!(x.get_unchecked(3), &2); // UB
}
}

该代码已经被修复为,不再使用 get_unchecked :


#![allow(unused)]
fn main() {
let data_ptr_end = data.as_ptr() as usize + data.len();
}

RUSTSEC-2021-0037: Diesel 库的 Sqlite 后端 UAF(use-after-free) bug

漏洞描述:

漏洞分析

Diesel 的 sqlite 后端使用了 libsqlite3_sys 这个库来调用 sqlite 提供的sql函数。比如sqlite3_finalizesqlite3_step 之类。

sqlite 函数执行调用过程:

  • sqlite3_open()
  • sqlite3_prepare()
  • sqlite3_step() // 用于执行有前面sqlite3_prepare创建的 预编译语句
  • sqlite3_column() // 从执行sqlite3_step()执行一个预编译语句得到的结果集的当前行中返回一个列
  • sqlite3_finalize() // 销毁前面被sqlite3_prepare创建的预编译语句
  • sqlite3_close()

Diesel 的 by_name 查询通用做法是将预编译语句的所有字段名称保存为字符串切片以备以后使用。

但是sqlite的行为是:

  • 返回的字符串指针一直有效,直到准备好的语句被 sqlite3_finalize() 销毁,
  • 或者直到第一次调用 sqlite3_step() 为特定运行自动重新预编译该语句,
  • 或者直到下一次调用 sqlite3_column_name()sqlite3_column_name16() 在同一列。

在之前版本的 Diesel 中,没有注意到这种情况,在调用 sqlite3_step() 之后,因为重新预编译语句,导致之前字符串切片指针就无效了。就造成 UAF 的情况。

这个案例告诉我们,在使用 FFi 的时候,要注意绑定sys库 的相关行为。这个在 Rust 编译器这边是无法检查发现的,案例应该属于逻辑 Bug。