让「安全」的Rust执行「不安全」的代码"

admin 2026-10-24 14:19:29 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

让「安全」的 Rust 执行「不安全」的代码

aeverj
aeverj

红队工坊

2026年10月23日 06:23
北京

去阅读

原文标题: pwning \"safe\" Rust into running my code

Rust 社区里有句话,大家应该都听过:

「Rust 是一门内存安全的编程语言。」

这话当然没错,至少在正常情况下是这样。

但我突然有了一个大胆的想法:能不能在完全不使用 unsafe 的情况下,让 Rust 执行任意代码?

而且,我还给自己加了几条限制:不使用 Nightly 实验性功能,不耍过程宏(Procedural Macro)的小聪明,也不靠 build.rs 构建脚本作弊。

只使用纯粹、安全、稳定版的 Rust,采用 2024 Edition。

结果呢?

还真能做到,而且做起来比想象中更离谱。

漏洞究竟出在哪里?

问题出在 Rust 类型系统的一个漏洞上。

GitHub 上已经有人报告了这个问题:

[rust-lang/rust#135011] https://github.com/rust-lang/rust/issues/135011

简单来说,Rust 的类型检查器(Type Checker)在处理循环定义的关联类型(Associated Type)时,存在一个类型健全性漏洞(Soundness Hole)。

正常情况下,编译器应该严格检查每个变量的类型,确保不同类型之间不会被随意混用。

但这里出现了一个问题。

当多个特征实现(Trait Implementation)互相引用,最终形成一个循环时,负责解析类型的类型规范化器(Type Normalizer)就可能被绕晕。

利用这个漏洞,我们可以欺骗编译器,让它把任意类型 L 当成任意类型 R。

熟悉 Rust 的朋友应该已经意识到这意味着什么了。

这不就相当于 std::mem::transmute 吗?

没错。

只不过,std::mem::transmute 正常情况下必须放在 unsafe 中调用,而我们的版本完全不需要。

核心调用代码长这样:

fn transmute<L, R>(x: L) -> R {
    transmute_inner_1::<L, R, RewriteReflRight<FakeRefl<L, R>>>(x)
}

当然,这只是最外层的入口函数。

背后还有大约 80 行特征定义,用来构造一份伪造的「类型相等证明」。

它的原理并不复杂:通过循环引用,让编译器无法真正把关联类型解析成具体类型,自然也就无法正确验证两个类型到底是不是相同。

但离谱的地方在于:

Rust 稳定版编译器居然真的接受了这段代码。

编译器仿佛在说:「嗯,看起来没什么问题,过吧。」

有了这个漏洞,我们能干什么?

答案是:几乎所有 std::mem::transmute 能做的危险操作,我们都能做。

例如:

• 伪造指针(Pointer Forgery)

• 构造虚假的虚函数表(vtable)

• 将任意字节重新解释为其他类型

• 绕过 Rust 原本应该提供的类型安全保证

换句话说,Rust 类型系统原本建立起来的安全边界,在这里被直接绕过了。

为了验证这个漏洞,我决定做一个经典实验:

伪造一个指向 libc::system() 的函数指针,然后直接调用它。

代码如下:

type SystemFn = extern \"C\" fn(*const u8) -> i32;

fn main() {
    let system_fn: SystemFn =
        transmute::<usize, SystemFn>(libc::system as *const () as usize);

    system_fn(b\"echo 'mrow :3'\\0\".as_ptr());
}

这段代码做了什么?

首先,我们定义了一个函数指针类型 SystemFn,它接受一个指针参数,并返回一个 32 位整数。

接着,我们获取 libc::system 的地址,把它转换为 usize,再通过刚才构造的 transmute,将这个整数变成一个可以调用的函数指针。

最后,调用这个函数指针,执行一条简单的 shell 命令。

运行结果:

[*] rustpwn - ACE in safe stable Rust
[*] rust-lang/rust#135011

mrow :3

成功了。

我们在 Rust 程序中实现了任意代码执行(Arbitrary Code Execution,ACE)的演示。

更重要的是,整个过程:

没有出现任何一个 unsafe 关键字。

代码可以在稳定版 Rust 上编译,也能正常运行。

即使执行:

grep -rn unsafe src/main.rs

也找不到任何结果。

甚至在代码中加入:

#[forbid(unsafe_code)]

编译器照样放行。

这意味着,即使一个项目明确禁止使用不安全 Rust 代码,也无法通过这个限制阻止漏洞被利用。

类型系统到底是怎么被骗过去的?

现在,我们来看看背后的原理。

整个技巧的核心,是通过多层特征构造一个假的类型相等证明,让编译器误以为:

L == R

也就是说,让 Rust 相信两个原本完全不同的类型其实是同一个类型。

为了做到这一点,代码使用了几个关键组件。

• TypeIs<T>:一个简单的类型恒等特征。对任何类型,它的关联类型 Is 都等于 T。

• TyEq / TyEqImpl:一组用于表达类型相等关系的特征,可以把它们理解成一套「类型相等证明」机制。

• Refl<T>:真正合法的自反性证明。也就是证明 T == T。这当然完全没问题。

• FakeRefl<L, R>:整个漏洞利用中最关键的部分。它试图伪造一份 L == R 的证明,并通过 DeriveEqFromImpl 将证明委托给 Proof<()>,从而形成循环。

• RewriteReflRight<P>:利用另一份证明的定义改写等式右侧的类型,借助编译器无法完整解析循环关联类型的缺陷,让伪造的证明通过检查。

听起来有点绕?

我们可以用一个简单的比喻理解。

正常情况下,如果你想证明 A 等于 B,就必须拿出一份真实有效的证明。

但 FakeRefl<L, R> 采用了另一种操作:

「你要证明 L 等于 R?没问题,证明就在 FakeRefl<L, R>::Proof<()> 里面。」

编译器于是去查看这个证明。

结果发现,这份证明又指向了自己。

就这样,证明形成了一个循环。

编译器沿着循环不断尝试解析,却无法得到能够正确验证的具体类型。

而漏洞恰恰就在这里:编译器没有因为这个无法正确完成的检查而拒绝代码,反而接受了这份证明。

于是,它最终被说服,认为 L 和 R 可以当成相同类型处理。

是不是很荒唐?

更离谱的还在后面。

最初报告这个漏洞的 steffahn,还成功把整个漏洞利用缩减到了下面这几行:

trait Tr<R>: Sized { type Prf: Tr<R, Prf = Self>; }
impl<L, R> Tr<R> for L {
    type Prf = R where L: Tr<R>, R: Tr<R, Prf = <L::Prf as Tr<R>>::Prf>;
}
fn transmute<L: Tr<R>, R>(r: L) -> <L::Prf as Tr<R>>::Prf { r }

没错。

仅仅 4 行稳定版 Rust 代码,就能实现一个不需要 unsafe 的 transmute。

这已经不是普通的类型转换技巧了,而是在利用类型系统自身的漏洞。

越往下挖,问题越严重

发现这个问题之后,我又去翻了翻 GitHub 上的漏洞讨论。

本来以为事情到这里就差不多了。

结果越看越不对劲。

■这个漏洞已经存在了 11 年

steffahn 发现,这类漏洞甚至可以追溯到 Rust 1.1。

那是什么时候发布的版本?

2015 年 6 月。

也就是说,这并不是某次新版本更新不小心引入的回归问题(Regression)。

它是 Rust 特征求解器(Trait Solver)中一个长期存在的根本性缺陷。

从 Rust 发展的早期开始,这个问题就已经存在。

直到 11 年后,我们依然能利用它。

■更糟糕的是,这还不止一个漏洞

Rust 类型团队的成员 lcnr 分析了这些精简后的漏洞复现代码,结果发现了另一件令人意外的事情。

steffahn 在尝试简化最初的漏洞时,竟然无意间发现了第二个独立的类型健全性漏洞。

lcnr 在讨论中指出:

这次精简实际上暴露了另一个独立的类型健全性问题。至少,我原本打算用来修复第一个漏洞的方法,并不足以解决这个问题。

也就是说:

最初的漏洞利用触发的是一个缺陷。

而后来缩减出来的 4 行代码,触发的却是另一个缺陷。

本来以为只有一个坑。

结果挖着挖着,发现旁边还有一个。

■优先级 P-medium,而且没有负责人

接下来是让我最难理解的部分。

我们已经证明,这个漏洞可以在完全安全的稳定版 Rust 中实现类似 transmute 的能力。

它可以造成任意类型混淆(Type Confusion),使任意代码执行成为可能,并破坏语言本应提供的类型安全保证。

那么,这个漏洞在 Rust 项目中被标记为什么优先级呢?

P-medium,中等优先级。

而且没有分配修复负责人。

这个问题自 2025 年 1 月被报告以来,一直处于这样的状态。

作为背景,截至 2026 年 5 月,Rust 官方仓库中还有超过 100 个尚未关闭、被标记为 I-unsound 的问题。

[查看 Rust 中的 I-unsound 问题] https://github.com/rust-lang/rust/labels/I-unsound

我们今天讨论的,只是其中一个。

■真正的修复可能需要重构整个特征求解器

为什么这样一个严重的漏洞没有被迅速修复?

因为问题可能比表面上复杂得多。

根据 lcnr 的分析,根本原因在于:

Rust 编译器会信任关联类型上声明的约束,却没有正确验证这个关联类型最终能否规范化为一个具体类型。

正常情况下,这些约束应该能够被验证。

但是,当某个关联类型通过循环引用不断指向自己时,它就可能无法被解析为最终结果。

这种情况下,相应的约束实际上并不成立。

可编译器没有正确检查这一点。

于是,类型系统就出现了可以被利用的漏洞。

另一位参与讨论的开发者 Nadrieril,则把这个问题与经典类型理论联系了起来。

他指出,这与非正递归(Non-positive Recursion)中的 ω 组合子(ω Combinator)有关。

[关于递归类型与相关类型理论的讨论] https://cs.stackexchange.com/questions/84307/what-are-the-rules-for-positive-recursive-types-in-dependent-type-theory/84312

简单来说,问题不只是某段代码写错了。

更深层的原因,可能在于类型系统本身的设计。

Rust 允许通过关联类型构造非正递归,而这种形式的递归在依赖类型理论(Dependent Type Theory)中已知可能导致类型系统失去健全性。

这意味着,修复它并不一定是加一个判断条件那么简单。

真正的解决方向,是 Rust 正在推进的特征系统重构项目:

[trait-system-refactor-initiative] https://github.com/rust-lang/trait-system-refactor-initiative/issues/62

其中一个核心工作,就是新的特征求解器 -Znext-solver。

但问题在于,这是一项规模庞大的重构工程。

它已经持续开发了很多年,当时仍然没有达到可以全面替代旧实现的程度。

所以,这个漏洞并不是没人知道。

而是修复它所涉及的问题,可能比漏洞本身还要棘手。

■最可怕的地方:任何第三方 Crate 都可能悄悄带入这个问题

现在,假设你是一个非常注重安全的 Rust 开发者。

你给整个项目加上:

#[forbid(unsafe_code)]

你要求所有代码都不能出现 unsafe。

你还会使用 cargo audit 检查依赖中的已知安全问题,使用 Clippy 做静态检查,甚至用 Miri 检查潜在的未定义行为。

听起来已经非常谨慎了,对吧?

然而,这个漏洞展示了一种更麻烦的情况。

任何发布在 crates.io 上的第三方 Crate(Rust 软件包),理论上都可以包含这样的 transmute 实现。

而引用它的下游项目,即使自身完全没有写 unsafe,也可能因此失去原本依赖的类型安全保证。

更麻烦的是,常见的检查手段不一定能发现它:

• cargo audit:主要检查依赖中已知且已被收录的安全漏洞,无法仅凭代码包含这种技巧就自动识别问题。

• #[forbid(unsafe_code)]:只禁止显式的不安全代码,但这里根本没有使用 unsafe。

• 搜索 unsafe 关键字:当然也没有用,因为代码中不存在这个关键字。

• Clippy:不会因为这种类型系统层面的欺骗就自动报警。

• Miri:虽然能够检查许多运行时未定义行为,但不能依靠源代码层面的常规检查直接识别这种伪造的类型关系。

问题就在这里。

从代码表面上看,一切都符合 Rust 的安全语法。

但类型系统已经被欺骗了。

也就是说,「没有使用 unsafe」并不必然意味着代码就能获得绝对的安全保证。

那么,真正的 Shellcode 呢?

既然已经能伪造函数指针,那能不能更进一步,直接执行 Shellcode?

我也尝试过。

这次我使用的是 AArch64 架构的 Shellcode,思路很直接:

  1. 使用 mmap 分配一块具有读、写、执行权限的内存页面,也就是 RWX 内存。

  2. 将机器指令写入这块内存。

  3. 跳转到对应地址,执行这些指令。

从原理上来说,这条路是可行的。

但实际测试时,我遇到了一个麻烦。

我使用的是 Apple Silicon 平台。

在 macOS 上,系统对即时编译(Just-In-Time Compilation,JIT)内存有额外的保护机制。

特别是针对可执行内存页面,macOS 会通过 MAP_JIT 和 pthread_jit_write_protect_np 等机制实施写入与执行权限方面的限制,也就是 W^X 保护策略。

这意味着,如果想完成整个 Shellcode 执行流程,我还需要伪造对这些系统接口的调用。

理论上可以做到。

但说实话,有点麻烦。

而且没太大必要。

因为之前通过 libc::system() 伪造函数指针的实验,已经足以证明核心问题:

我们可以在不使用 unsafe 的情况下,绕过 Rust 的类型安全限制,实现任意代码执行。

至于在 x86\_64 Linux 上,如果运行环境允许通过 mmap 分配 RWX 内存页面,那么直接执行原始 Shellcode 理论上应该更加简单。

最后的一些感想

「用 Rust 重写吧!」他们说。

「Rust 是内存安全的!」他们说。

但现实是:

一个已经存在 11 年的类型系统漏洞。

被标记为 P-medium。

没有分配负责人。

从 2025 年 1 月被公开报告,到 2026 年 5 月仍然可以复现。

代码依旧能够编译。

程序依旧能够运行。

当然,这并不意味着 Rust 的内存安全设计毫无价值,也不代表所有 Rust 程序都不安全。

但这次实验至少说明了一件事:

即使一门语言把内存安全作为核心设计目标,其编译器和类型系统本身也可能存在漏洞。

当安全保证建立在编译器正确性的基础上时,一旦编译器接受了本应拒绝的类型关系,那些看似牢不可破的安全边界就可能失效。

而最有意思,也最令人担忧的地方在于:

我们整个实验,甚至没有写过一个 unsafe。

完整源代码:

[查看 rustpwn 项目] https://code.wejust.rest/lain/rustpwn

如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号

在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!

    让「安全」的Rust执行「不安全」的代码" 网络安全文章

    让「安全」的Rust执行「不安全」的代码"

    本文揭示Rust类型系统存在健全性漏洞,可在完全不使用unsafe的稳定版Rust中实现任意代码执行。漏洞源于类型检查器处理循环关联类型时的缺陷,可伪造类型相等证明实现类似transmute的功能。该漏洞已存在11年,标记为P-medium优先级,修复需重构特征求解器。第三方crate可能悄悄引入此问题,常规安全检查手段难以发现。
    工具|AutoHunter" 网络安全文章

    工具|AutoHunter"

    本文介绍AutoHunter,一个多Agent协同的自动化漏洞挖掘系统,支持24×7无人值守、真实工具链挖洞、AI初审、通杀Hunter、情报沉淀复用、内置WAF与鉴权及数据库备份等功能,并提供GitHub项目地址。工具仅供安全研究与学习使用,使用者需承担全部法律责任。
    LiteLLM1.107.0遭供应链攻击入侵:凭据窃取器与后门通过PyPI与Docker分发" 网络安全文章

    LiteLLM1.107.0遭供应链攻击入侵:凭据窃取器与后门通过PyPI与Docker分发"

    LiteLLM1.107.0遭供应链攻击,攻击者利用被盗凭据通过PyPI和Docker分发恶意版本,植入凭据窃取器privcollect与远程代码后门,影响所有平台wheel及Docker镜像。恶意载荷通过.pth和sitecustomize.py双重启动钩子自动执行,窃取浏览器密码、云凭据及LiteLLM数据库密钥。建议立即检查环境、固定版本至1.106.2并轮换所有可能暴露的凭据。
    评论:0   参与:  0