深入理解 Aptos Move 中的重入攻击: 从 EVM 噩梦到 Move 的多层防御
2026/7/25 12:47:35 网站建设 项目流程

深入理解 Aptos Move 中的重入攻击:从 EVM 噩梦到 Move 的多层防御

如果你在 Web3 领域待得够久,一定知道“重入(reentrancy)”这个词的分量。2016 年,正是它让 The DAO 损失了 6000 万美元,此后它也一直是无数漏洞的罪魁祸首。当我第一次接触 Aptos 时,脑子里冒出的第一个问题就是:“Move 真的能解决重入问题吗,还是说这仅仅是营销话术?”

简短的回答是,事情没那么简单。Move 并没有在所有可能场景下让重入变得不可能,但它确实让攻击难度大幅提升,而且 Aptos 虚拟机(VM)内置了多层防护机制。接下来,我会通过真实的代码,带你一步步看清这一切。


重入攻击到底是什么?

在深入 Move 的方案之前,我们先统一概念。重入攻击发生在这样的场景:合约在更新自身状态之前调用了外部合约,而被调用的合约又恶意地回调了原合约,此时第一次调用尚未结束。

经典的 Solidity 漏洞代码如下:

function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount); (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] -= amount; // 状态更新在外部调用之后 }

攻击者的 fallback 函数会在balances[msg.sender]更新之前再次调用withdraw(),从而把合约掏空。

问题的核心在于:状态更新发生在了外部调用之后。合约在内部状态尚未一致的情况下,就将执行控制权交给了不可信的一方。


Move 的设计哲学:从源头杜绝隐患

Move 从零开始就将安全性作为一等公民。语言的资源导向模型(resource-oriented model)旨在从架构上让整类攻击变得困难甚至不可能,包括重入、双花、算术溢出等。

其核心理念是:在 Move 中,资源不能被隐式复制或丢弃。每个资源都必须显式地从一个位置移动到另一个位置。这种线性类型系统确保同一时刻只能有一个执行上下文访问该资源。

事情在早期版本中更为简单,那时 Move 根本不支持动态分派,只允许静态分派,即模块依赖必须在编译时声明。由于 Move 强制要求依赖图无环,重入攻击实际上毫无可能。社区当时理所当然地认为 Move “天生免疫”重入。


转折:原生动态分派的引入

随着 Aptos 生态逐渐壮大,语言也获得了更强的表达能力。通过原生函数(native functions)——即 Aptos 虚拟机中用 Rust 实现、可从 Move 代码调用的函数——动态分派成为了可能。这些原生函数能执行纯 Move 之外的操作,包括动态分派。

例如,Aptos 框架提供了dispatchable_fungible_asset模块,足以支撑完整的动态分派能力。乍看之下,这似乎又为重入攻击打开了理论上的大门。

但 Aptos 团队并没有听天由命,而是在虚拟机内部直接构建了一套防御机制。


ReentrancyChecker:Aptos VM 如何保护你

Aptos VM 内置了一个专门用来防范重入攻击的ReentrancyChecker。它维护了两个关键数据结构:

  1. 活跃模块映射(active modules map):记录当前调用栈中每个模块出现了多少次。
  2. 模块锁计数(module lock count):记录“模块锁定模式”的状态。

其核心逻辑在enter_function函数中,该函数在每次 Move 函数调用时都会被触发。简化后的代码如下:

pubfnenter_function(&mutself,caller_module:Option<&ModuleId>,callee:&LoadedFunction,call_type:CallType,)->PartialVMResult<()>{ifcall_type==CallType::NativeDynamicDispatch||callee.function.has_module_reentrancy_lock{// 对于原生动态分派或标记了锁的函数,进入模块锁定模式self.enter_module_lock()}letcallee_module=callee.module_or_script_id();ifSome(callee_module)!=caller_module{// 跨模块调用// 当模块锁处于激活状态,且该模块已在调用栈中时,禁止重入matchself.active_modules.entry(callee_module.clone()){// ... 检查并阻止重入}}}

当模块锁激活时,VM 会检查你是否试图重入一个已经在调用栈中的模块。如果是,交易会被回滚,并抛出“Reentrancy disallowed”错误。

该检查器可以区分直接重入(模块 A 调用自身)和间接重入(模块 A → 模块 B → 模块 A),在模块锁激活时,两者均被禁止。


完整示例:一个看似脆弱的模式

让我展示一个在 Move 中可能存在的脆弱模式,以及 VM 的保护机制为何如此重要。

假设有一个简单的金库合约,允许用户存取代币:

module 0x42::vault { use aptos_framework::coin; use aptos_framework::signer; struct Vault has key { balances: table::Table<address, u64>, } public entry fun deposit(user: &signer, amount: u64) acquires Vault { let addr = signer::address_of(user); let vault = borrow_global_mut<Vault>(@vault_addr); // 将代币从用户转入金库 coin::transfer<0x1::aptos_coin::AptosCoin>(user, @vault_addr, amount); // 更新余额 let current = table::borrow_mut(&mut vault.balances, addr); *current = *current + amount; } public entry fun withdraw(user: &signer, amount: u64) acquires Vault { let addr = signer::address_of(user); let vault = borrow_global_mut<Vault>(@vault_addr); let balance = table::borrow(&vault.balances, addr); assert!(*balance >= amount, 1); // 将代币从金库转给用户 coin::transfer<0x1::aptos_coin::AptosCoin>(&@vault_addr, addr, amount); // **转账之后**再更新余额 let current = table::borrow_mut(&mut vault.balances, addr); *current = *current - amount; } }

注意问题所在:余额更新发生在代币转账之后。在 Solidity 中,这就是经典的重入漏洞,攻击者可以在代币转账过程中回调withdraw函数,从而耗尽金库。

但在 Move 中,事情并非如此。

为什么?因为coin::transfer是一个原生函数,它并不会将控制权交给不可信的合约。转账过程在 VM 内部原子性地完成,没有“回调”机制能让接收方在转账过程中执行任意代码。


真正的威胁:跨模块重入

Move 中更现实的威胁来自跨模块调用:模块 A 调用了模块 B 的某个函数,而模块 B 又回调了模块 A。这时ReentrancyChecker就至关重要了。

设想如下场景:

// 模块 A:金库 module 0x42::vault { use 0x43::callback_router; public fun withdraw_with_hook(user: &signer, amount: u64) { // ... 转账代币 ... // 调用外部钩子 callback_router::after_withdrawal(user, amount); } } // 模块 B:回调路由器(可能是恶意的) module 0x43::callback_router { use 0x42::vault; public fun after_withdrawal(user: &signer, amount: u64) { // 这能否回调 vault::withdraw ? vault::withdraw(user, amount); // 尝试重入 } }

如果模块 B 试图在跨模块调用过程中重入模块 A,ReentrancyChecker会立即捕获。当模块锁激活,且模块 A 已经在调用栈中时,VM 会阻止这次重入。


真实世界的警钟

2026 年 2 月,安全公司 Hexens 发现 Aptos Move VM 本身存在一个严重漏洞,一个“缓存过期(stale-cache)”问题,可能引发类型混淆攻击。研究人员估计,一台价值 3000 美元的服务器在模拟环境中能以近 90% 的成功率实施攻击。

该漏洞通过 Aptos 的漏洞赏金计划上报,并在数小时内被修复,没有任何资金损失。这个案例印证了一个重要观点:尽管 Move 从设计上消除了许多漏洞类型,但虚拟机和运行时环境仍然需要严谨的安全实践。


给 Aptos 开发者的实用建议

基于以上分析,我给 Aptos 开发者提出以下建议:

1. 理解保护机制,但别盲目依赖

ReentrancyChecker能防范跨模块重入,但它并非万能。始终遵循“检查-生效-交互”(checks-effects-interactions)模式:先验证输入,再更新状态,最后进行外部交互。

2. 正确使用访问控制

任何Object<T>都可以被任何人访问,任何人都可以把任意Object<T>传给任何函数。务必验证签名者是否为合法所有者:

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

3. 遵循最小权限原则

从私有函数开始,仅在必要时扩大可见性。对于受控的模块间访问,使用public(friend)。除非需要从 CLI 或 SDK 调用,否则不要将函数标记为entry

4. 充分利用 Move Prover

Move Prover 能自动检查你的代码是否不存在某些类型的错误,包括重入漏洞。请务必使用它。Aptos 框架本身就附带了标准库的 Move Prover 规范。

5. 持续关注 VM 的更新

Aptos VM 在不断演进。ReentrancyChecker的实现可能会发生变化,新的攻击途径也可能出现。请关注 Aptos 核心代码仓库和安全公告。


总结

Move 并没有让重入变得不可能,但它让重入变得比 Solidity 中困难得多。资源模型杜绝了最常见的攻击模式,而ReentrancyChecker则提供了运行时的跨模块重入防护。

Move 真正的安全优势并不在于它能消灭所有漏洞,而在于它能从设计上消灭整类漏洞。我们不再需要依赖开发者时刻谨记“检查-生效-交互”模式(他们总会忘记,审计师也会遗漏),Move 让许多攻击在架构层面变得困难甚至不可能。

这并非安全的保证,而是一个巨大的先发优势。

请编写安全的代码,善用一切可用工具,并且永远不要认为“语言已经帮我防住了”就意味着你可以不用思考安全问题。2026 年 2 月那场险些造成 700 亿美元损失的漏洞事件,就是最好的证明。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询