- AnyswapVRouter 授权漏洞: 绕过了不存在的 permit 函数 AnyswapVRouter 授权漏洞绕过了不存在的 permit 函数在区块链安全的世界里漏洞往往藏在那些“看似合理”的假设中。今天我们要聊的是一个发生在跨链桥协议 Anyswap 中的经典案例——AnyswapVRouter合约的授权漏洞。攻击者利用了一个不存在的permit函数成功绕过了授权检查盗走了大量用户资金。这个故事听起来像“空城计”但却是真实发生过的。### 漏洞背景跨链桥的信任模型Anyswap 是一个去中心化跨链协议允许用户在不同链之间转移资产。它的核心组件之一是AnyswapVRouter负责处理跨链交易中的代币转移和授权逻辑。在以太坊生态中ERC20标准定义了approve和transferFrom机制用于授权第三方比如合约代为转账。而permit函数则是一个扩展允许用户通过签名授权而不必发送一笔真实的交易节省 Gas。但问题来了——如果合约代码中根本没有实现permit函数但业务逻辑却错误地调用了它会发生什么答案是攻击者可以伪造一个假的permit调用让合约误以为用户已经授权了从而盗走代币。### 漏洞原理凭空捏造的 permit我们先来看一个简化版的AnyswapVRouter合约逻辑用 Solidity 伪代码表示solidity// SPDX-License-Identifier: MITpragma solidity ^0.8.0;interface IERC20 { function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;}contract AnyswapVRouter { // 假设这是一个用于跨链转账的函数 function swapAndTransfer( address token, address recipient, uint256 amount, // 以下是攻击者传入的“签名参数” uint8 v, bytes32 r, bytes32 s ) external { // 关键错误这里调用了 permit但 token 可能根本没有实现 permit IERC20(token).permit(msg.sender, address(this), amount, block.timestamp 1000, v, r, s); // 然后执行转账 IERC20(token).transferFrom(msg.sender, recipient, amount); // 后续逻辑... }}漏洞核心合约在转账前先调用了permit函数目的是让用户通过签名授权省去先approve的步骤。但问题是如果token合约是一个标准的ERC20没有实现permit那么调用permit会直接 revert。但攻击者可以1. 部署一个恶意代币合约其中实现了permit函数但逻辑是“无条件通过”。2. 用这个恶意代币作为token参数调用swapAndTransfer。3. 因为恶意代币的permit直接返回成功合约就认为用户已经授权了然后调用transferFrom从msg.sender转走代币。但等等攻击者为什么要转自己的钱真正的攻击场景是攻击者利用这个漏洞欺骗合约从其他用户那里转账。这就需要更复杂的攻击路径比如合约中还有callback或swap逻辑允许攻击者控制msg.sender的身份。### 实际攻击路径借刀杀人在真实攻击中攻击者利用了AnyswapVRouter中的另一个功能——通过swap合约调用transferFrom时msg.sender是攻击者控制的合约地址。具体流程1. 攻击者创建了一个恶意合约AttackerContract它伪装成合法的swap模块。2. 攻击者调用AnyswapVRouter.swapAndTransfer并传入token为受害者的真实代币比如USDC但recipient设置为攻击者自己的地址。3. 关键攻击者传入的v, r, s参数是随意构造的但合约调用了token.permit。如果USDC没有permit就会 revert。但攻击者可以让token地址指向一个攻击者部署的恶意合约而该合约的permit函数直接跳过任何检查。4. 然后合约调用transferFrom(msg.sender, recipient, amount)此时msg.sender是AnyswapVRouter合约自身因为外部调用者是通过swap模块进入的所以transferFrom的sender是AnyswapVRouter而不是用户。这不会直接盗取用户资金。真正的漏洞利用攻击者需要找到一种方式让transferFrom的sender是受害者。这需要合约中有一个函数允许外部用户传入sender地址。在 Anyswap 的实际代码中确实存在这样的逻辑——某些函数允许用户指定from地址比如为了跨链赎回但没有验证这个from是否已经被授权。结合permit漏洞攻击者可以solidity// 攻击者调用这个函数function redeem(address token, address from, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { // 利用不存在的 permit 来绕过授权 IERC20(token).permit(from, address(this), amount, block.timestamp, v, r, s); // 直接从 from 转走代币 IERC20(token).transferFrom(from, msg.sender, amount);}因为permit被错误地调用且token可能是攻击者部署的恶意合约permit直接成功合约就认为from已经授权了从而转走受害者的代币。### 代码示例攻击者如何伪造 permit下面是一个完整的攻击示例用 Python 模拟逻辑但核心思想相同python# 模拟攻击过程class MaliciousToken: def permit(self, owner, spender, value, deadline, v, r, s): # 恶意实现不做任何验证直接返回成功 print(恶意 permit 被调用直接通过) return True def transferFrom(self, sender, recipient, amount): # 这里我们假设可以转走任何人的代币 print(f从 {sender} 转走 {amount} 个代币到 {recipient}) return Trueclass VRouter: def __init__(self): self.token MaliciousToken() def redeem(self, from_addr, amount, v, r, s): # 漏洞代码调用了不存在的 permit self.token.permit(from_addr, self, amount, 12345, v, r, s) self.token.transferFrom(from_addr, self.msg_sender, amount)# 攻击者调用router VRouter()router.msg_sender attackerrouter.redeem(from_addrvictim, amount1000, v0, rb, sb)# 输出: 恶意 permit 被调用直接通过# 输出: 从 victim 转走 1000 个代币到 attacker### 修复方案检查函数是否存在修复这个漏洞的关键是在调用permit之前先检查该代币是否实现了permit函数。可以使用 Solidity 的address.code.length或try-catch机制solidityfunction safePermit( address token, address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) internal { // 检查合约是否有代码 if (token.code.length 0) { // 尝试调用 permit如果 revert 则说明不支持 try IERC20(token).permit(owner, spender, value, deadline, v, r, s) { // 成功 } catch { // 不支持 permit 时回退到传统的 approve transferFrom // 要求用户先手动 approve require(IERC20(token).allowance(owner, address(this)) value, allowance not enough); } } else { revert(Invalid token); }}### 总结这个漏洞的本质是对合约接口的过度信任。开发者假设所有代币都实现了permit但现实是很多 ERC20 代币并没有。攻击者利用这一点通过部署恶意代币或构造假的签名参数绕过了授权检查。给开发者的启示1.永远不要假设外部合约实现了某个接口除非你明确知道它符合标准。2. 调用外部函数时使用try-catch或检查address.code.length。3. 对于涉及资金转移的授权逻辑一定要做多重验证。给用户的启示在跨链桥或 DeFi 协议中即使你授权了代币也要定期检查授权额度并关注协议是否有安全审计。毕竟代码里的“空城计”防不胜防。