Aptos 生态系统中的 Move 智能合约安全:审计、攻击和最佳实践

这篇文章深入探讨了 Aptos 生态系统中 Move 智能合约的安全性,指出 Move 虽消除了 Solidity 的部分缺陷,但也带来了新的安全模型。内容涵盖了 Move 特有的漏洞类型、常见的错误、实际的审计方法论、攻击场景及安全最佳实践。

Move 消除了 Solidity 一些最糟糕的故障模式,但它引入了一种不同的安全模型,许多 Aptos 团队仍然误读。本指南涵盖 Move 特定的漏洞、攻击路径和实际审计方法。

JohnnyTime

Move 比 Solidity 更安全,就像一辆刹车更好的赛车比没有刹车的赛车更安全一样。有用。重要。但仍然不等于安全。

这种区别很重要,因为 Move 消除了一些糟糕的 EVM 故障模式,同时引入了一套不同的安全假设。资源、能力、所有权和模块边界为审计师提供了比 Solidity 通常更强的保证。它们并未消除对能力设计、对象所有权、泛型类型绑定、升级控制和恶意组合进行推理的必要性。

本指南是这种现实的实用版本。它涵盖了 Move 的优点、团队仍然受损的地方、值得实际审计时间的漏洞类别,以及严肃的 Aptos 审查应遵循的方法。

目标读者

本文面向两类人群:

  • 希望了解真实 Move 安全审查应涵盖内容的 Aptos 团队。
  • 来自 Solidity 的审计师,在审查以对象为主的 Move 代码之前需要更清晰的思维模型。

如果你已经熟悉 EVM 审计,请关注 Move 改变安全边界的地方,而不是它仅仅感觉更简洁的地方。

Move 的安全模型:优势与盲点

Move 值得尊重的原因很简单:它将更多的安全保证推入语言本身。

其设计的核心是资源、能力、所有权和模块边界。

资源强制显式资产处理

在 Solidity 中,稀缺性主要通过状态和约定来模拟。在 Move 中,稀缺性是类型系统的一部分。

资源不能被复制,除非它具有 copy 能力。它不能无声地消失,除非它具有 drop 能力。它不能存在于存储数据中,除非它具有 store 能力,并且不能直接用于全局存储操作,除非它具有 key 能力。

Move value model

Resource type
  - no copy  -> cannot be duplicated
  - no drop  -> cannot silently vanish
  - key      -> can participate in global storage ops

Capability / ordinary value
  - copy/drop often allowed
  - easier to pass around
  - should never be confused with an owned asset

能力是具有重大安全影响的简洁语法

这四种能力并非无关紧要。它们是威胁模型的一部分。

能力 允许的操作 审计师关注的原因
copy 复制一个值 对类似资产的状态很危险
drop 丢弃一个值 对义务和收据很危险
store 将一个值放入存储数据中 影响持久性和组合性
key 在全局存储操作中使用一个值 影响对链上状态的权限

Aptos Move 能力文档值得重新审视,因为能力组合是看似微小的设计选择成为真实攻击面的地方。

Move 消除了部分 EVM 风险,但并未消除对抗性审查的必要性

与 Solidity 相比,Move 在几个重要领域为开发者提供了真正的帮助:

  • 没有不受约束的 delegatecall 式危险。
  • 更强的所有权语义。
  • 更好地防止意外资产复制。
  • 更强的模块封装。
  • 更清晰的全局存储访问边界。

这就是为什么 Move 代码在审查时通常感觉更简洁。

但简洁不等于正确。错误转移到了能力边界、对象所有权、泛型、可变引用和操作控制中。

团队通常在 Move 安全方面犯的错误

最常见的 Move 错误并非来自对 VM 操作码层面的误解。它们来自将语言保证视为比实际更广泛。

错误 1:将 &signer 视为完整的授权

接受一个签名者与证明该签名者应该被允许执行该操作是不同的。

Aptos Move 安全指南明确指出:对于敏感操作,你仍然需要验证签名者是预期的账户或所操作对象的合法所有者。

错误 2:混淆对象访问与所有权证明

Object<T> 传递给函数并不能证明调用者拥有该对象所代表的资产或权限。

这种微妙之处产生了一种非常 Move 特有的错误类别:对象所有权检查失败。一个质押对象、订阅对象或抵押对象可以是有效的,但仍然属于其他人。

错误 3:将泛型视为单纯的开发者便利

泛型是安全边界的一部分。

如果收据未与借入 Token 的资产类型进行参数化,则还款路径可能仅证明有某个 Token 被返还,而不是证明返还了正确的 Token。

4:假设不变量在可变交接后仍然有效

如果你验证了一个不变量,然后将 &mut T 跨越信任边界传递,你就不再控制该不变量。

被调用者可能无法解包你的私有字段,但它仍然可以替换整个值、通过其他路径修改状态或使你刚刚检查的假设失效。

值得认真审计的 Move 漏洞类别

1. 对象所有权检查失败

这是 Move 安全并非仅仅是“接受签名者然后继续”的最清晰例子之一。

entry fun execute_action_with_valid_subscription(
    user: &signer,
    obj: Object<Subscription>
) acquires Subscription {
    let object_address = object::object_address(&obj);
    let subscription = borrow_global<Subscription>(object_address);
    assert!(subscription.end_subscription >= timestamp::now_seconds(), 1);
    // action continues...
}

这检查了订阅是否存在且处于活动状态。它没有检查调用者是否拥有它。

缺失的防护措施在概念上很简单:

assert!(object::owner(&obj) == signer::address_of(user), ENOT_OWNER);

如果你跳过此检查,一个有效的对象可能会成为权限绕过。

2. 资产流中的泛型类型不匹配

一个有漏洞的闪电贷设计可能会返回 (Coin<T>, Receipt),然后接受 repay_flash_loan<T>(receipt: Receipt, coins: Coin<T>)

问题在于 Receipt 实际上并未绑定到 T,因此协议仅证明存在还款金额,而不是还款资产与借入资产匹配。

struct Receipt<phantom T> has drop {
    amount: u64,
}

public fun flash_loan<T>(amount: u64): (Coin<T>, Receipt<T>) { /* ... */ }
public fun repay_flash_loan<T>(receipt: Receipt<T>, coins: Coin<T>) { /* ... */ }

phantom 参数很重要,因为它将业务规则推入类型系统。

3. 危险的能力分配

copy 赋予类似资产的类型,或将 drop 赋予类似义务的类型,是严重的设计错误。

  • 对类似资产的值进行 copy 操作可能会导致通货膨胀或双重支付行为。
  • 对义务或收据进行 drop 操作可能会让借款人丢弃他们本应履行的证明。

这是 Move 安全故事只有在类型设计者做出正确选择时才有效的地方之一。

4. ConstructorRef 泄露

在创建 Aptos 对象时,暴露 ConstructorRef 可能会泄露超出团队预期的更多控制权。根据流程,该引用稍后可能会被转换为更强的修改或转移能力。

对于 NFT、托管和以对象为主的协议,规则很简单:除非你已对每个派生能力的完整生命周期进行建模,否则不要返回或随意持久化 ConstructorRef

5. 可变引用、函数值和延迟执行

Move 在结构上仍然比 Solidity 更能抵抗经典的重入攻击,但这并不意味着所有状态交接问题都消失了。

审计师仍然需要考虑:

  • &mut 引用跨越信任边界。
  • 回调和延迟执行。
  • 重新引入检查时与使用时问题(time-of-check versus time-of-use)的函数值。
  • 在交接前有效但在交接后失效的状态假设。

正确的结论不是“Move 现在有重入问题了”。而是“Move 有比许多团队预期更多的状态假设失效方式”。

6. 操作和升级假设

并非所有严重的 Move 错误都与类型有关。一次真正的审查还应包括:

  • 包升级控制。
  • 环境之间的发布密钥分离。
  • 预言机和定价假设。
  • 无界迭代和拒绝服务风险。
  • 费用舍入和精度问题。
  • 当多个资源一起移动时的共享对象-账户拓扑。

如果升级密钥薄弱,即使是干净的 Move 代码也可能成为管理层“rug pull”的载体。

两个真实的漏洞利用场景

场景 1:通过 &mut 进行无价值资产替换

此场景基于 Aptos 当前关于传递给不受信任代码的可变引用的指导。

设置

一个协议接受一个 FungibleAsset,检查它是否是预期的资产,然后将 &mut FungibleAsset 传递给一个Hook。Hook返回后,协议假定资产身份未改变。

攻击流程


1. 用户存入合法资产。

2. 协议验证元数据一次。

3. 协议将 &mut 资产传递给攻击者控制的逻辑。

4. 攻击者用无价值资产替换该值。

5. 协议恢复并根据现在无价值的资产铸造信用。

6. 金库收到垃圾资产。攻击者保留真实价值。

审计师应标记的内容

  • &mut 传递给不受信任回调的公共 API。
  • 在交接前检查但在交接后未检查的不变量。
  • 假设身份在调用后未改变的信用、抵押或金库逻辑。

场景 2:泄露的 ConstructorRef 成为未来的追回路径

这对于 NFT 市场、托管系统和以对象为主的 DeFi 设计很重要。

设置

一个铸币函数为了方便返回一个 ConstructorRef。团队认为这无害,因为对象已经成功创建。

攻击流程


1. 铸币者收到或泄露一个 ConstructorRef。

2. ConstructorRef 被用于派生一个更强大的能力。

3. 对象稍后被出售或作为抵押品发布。

4. 原始行为者仍然持有一个隐藏的控制路径。

5. 资产在转移后被追回、重定向或修改。

审计师应标记的内容

  • 任何返回或存储 ConstructorRef 的函数。
  • 比对象创建生命周期更长的派生能力。
  • 假设“已铸造并转移”意味着“未来的控制面已移除”的系统。

严肃的 Move 审计应包括什么

如果你正在审计基于 Move 的协议,从第一天起,这个过程就应该与 Solidity 审查有所不同。

1. 映射资源、对象、能力和入口点

在深入阅读业务逻辑之前,请列举:

  • 每个 public entry 函数。
  • 每个 public(friend) 函数。
  • 每个具有 keystorecopydrop 的类型。
  • 每个类似能力的结构体。
  • 每个对象创建路径。
  • 每个特权签名者或管理员地址。
  • 每个升级路径和发布密钥。

在 Aptos 上,这张地图不是内务管理。它是审计工作的一半。

2. 在寻找错误之前推导不变量

对于每个协议,定义必须保持真实的“真理”:

  • 资产守恒。
  • 收据和义务守恒。
  • 授权边界。
  • 能力唯一性。
  • 对象所有权预期。
  • 抵押规则。
  • 预言机新鲜度假设。
  • 升级和暂停保证。

如果团队无法清晰地陈述其不变量,审计就已经告诉你一些重要信息。

3. 在关键之处强调类型系统

关注:

  • 泛型参数绑定。
  • 幻影类型。
  • 能力分配。
  • 签名者使用。
  • 资源生命周期。
  • 对象账户布局。
  • 能力是狭窄的还是过于强大的。

核心问题始终是:团队是否让类型系统强制执行业务规则,还是他们只是希望运行时逻辑能保持其完整性?

4. 像访问控制边界一样审查信任边界

在 Move 中,信任边界不仅仅是明显的管理功能。它们还包括:

  • &mut 传递给另一个模块。
  • 暴露回调或函数值。
  • 接受用户提供的对象。
  • 依赖预言机或桥接。
  • 依赖链下操作员进行升级或排序。

如果一个有状态的假设跨越了这些边界之一,那么在证明其不会被破坏之前,请假定它可能会被破坏。

5. 测试中止行为,而不仅仅是成功路径

Move 通常通过中止而不是静默损坏来失败。这更安全,但如果协议依赖于活性,仍然很危险。

至少审查以下情况:

  • 有效用户流上的意外中止。
  • 导致费用绕过的精度损失。
  • 成为拒绝服务向量的代码路径。
  • 用户控制结构上的无界循环。
  • 通过中止或 Gas 不对称可能产生偏差的随机性流。

6. 审查升级和操作安全

你仍然需要知道:

  • 谁控制升级。
  • 测试网和主网的发布密钥是否分离。
  • 暂停机制在压力下是否有效。
  • 治理是否可以静默修改关键参数。
  • 应急预案是否可信。

如果操作层薄弱,即使仓库中最干净的代码也可能无关紧要。

Move 审计实用清单

Move audit checklist

1. 列举每个 public 和 friend 入口点。
2. 列出每个资源及其能力。
3. 验证签名者检查和对象所有权检查。
4. 将所有资产敏感的收据和凭证绑定到正确的类型。
5. 审查每个能力,以评估其影响范围和最小权限。
6. 在任何不受信任的可变交接后重新检查不变量。
7. 审查回调、函数值和延迟执行路径。
8. 对算术、舍入和中止行为进行压力测试。
9. 审查预言机、桥接和升级假设。
10. 证明或测试在经济上真正重要的不变量。

工具:证明器、测试和模糊测试

正确的 Move 审计堆栈不是“仔细阅读代码然后祈祷”。

Move 证明器最适用于重要的不变量

Move 证明器指南很有用,但更重要的问题是你选择证明什么。

将其用于在实际漏洞利用中很重要的属性:

  • 余额守恒。
  • 能力唯一性。
  • 铸币、销毁、转移或管理流程后的关键后置条件。
  • 在规定前置条件下没有意外中止。

单元测试应模拟对抗性序列

功能正确性是不够的。好的测试套件应模拟:

  • 错误的对象所有者。
  • 错误的泛型资产类型。
  • 过时或被操纵的预言机值。
  • 重复回调。
  • 中止的还款流程。
  • 验证后的资产替换尝试。

模糊测试仍有增加价值的空间

Move 尚未拥有与 EVM 生态系统相同的成熟模糊测试文化,这使得不变量驱动的模糊测试更有价值。

有用的序列包括:

  • 存款 -> 借款 -> 还款 -> 清算。
  • 铸币 -> 转移 -> 托管 -> 赎回。
  • 创建对象 -> 派生引用 -> 转移所有权。
  • 回调驱动的流程,其中状态可能在检查和使用之间发生变化。

值得编码到设计中的安全模式

1. 使用狭窄的能力

不要创建一个可以暂停系统、铸造供应、提取金库资金和管理升级的能力。分离权限,这样单个泄露的能力就不会造成灾难性后果。

2. 将业务含义绑定到类型

如果收据属于某种资产类型,请将其编码到类型中。如果凭证代表固定的未来行动,请编码资产和金额,以便运行时逻辑减少猜测。

3. 在适用时优先使用签名者绑定的存储访问

当设计允许时,直接从 signer::address_of(user) 借用或移动数据通常比接受任意对象并希望调用者提供了正确的对象更安全。

4. 跨越信任边界后重新验证

如果一个不变量在调用前很重要,那么在调用后它仍然很重要。每当可变或延迟执行路径可能改变身份、余额、权限和边界时,请重新检查它们。

5. 有意设计对象拓扑

共享对象账户不仅仅是一种组织技巧。它们影响转移和修改语义,因此它们属于威胁模型。

总结

Move 在很多方面都做得很好。资源、能力、所有权和模块边界消除了多年来困扰 EVM 协议的真实错误类别。

但剩下的错误并非表面问题。它们存在于对象所有权、能力设计、泛型类型绑定、可变信任边界和升级控制中。这就是为什么一次真正的 Move 审计应该与一份回收的 Solidity 清单有所不同。它应该具备资源意识、对象意识、能力意识,并坚定不移地关注不变量。

  • 原文链接: smartcontractshacking.co...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
JohnnyTime
JohnnyTime
江湖只有他的大名,没有他的介绍。