Appearance
CTF 挑战:Cashback——揭秘链上银行的“双重返利”潘多拉魔盒
在加密世界的浩瀚星海中,DeFi创新层出不穷,而“Cashback”这家号称“城里最火爆的加密新银行”无疑是最耀眼的新星之一。它以诱人的承诺吸引着无数用户:每次链上支付都能赚取积分,积分累计到一定程度,便可晋升“传奇”地位,解锁梦寐以求的“超级返利NFT”徽章。
这听起来像是一个完美的数字黄金时代。但作为CTF挑战者,我们的任务并非安享返利,而是要成为这个忠诚度计划的“噩梦”:发掘隐藏的后门,在所有支持的货币中刷满返利,并最终攫取至少两个超级返利NFT,其中一个必须归属于我们自己的玩家地址。
一场围绕EIP-7702和智能合约漏洞的狩猎,就此拉开序幕!
Cashback的诱惑与陷阱:EIP-7702下的智能账户逻辑
Cashback的核心机制在于其巧妙利用EIP-7702标准,允许EOA(外部账户)像智能合约一样拥有更强大的功能,实现所谓的“智能账户”。用户需要将他们的EOA“委托”给Cashback合约,才能使用 payWithCashback 函数进行支付并累积返利。
合约设计解析:
积分累积 (
accrueCashback):- 这个函数负责根据支付金额和货币类型计算返利,并铸造对应的ERC1155代币(代表积分)。
- 关键在于,当
consumeNonce()返回SUPERCASHBACK_NONCE(10000) 时,会触发一个超级返利NFT的铸造。 - 它受到严格的修饰符限制:
onlyDelegatedToCashback,onlyUnlocked,onlyOnCashback。
支付入口 (
payWithCashback):- 这是用户发起支付的入口,它会调用
currency.transfer进行实际转账,然后内部调用CASHBACK_ACCOUNT.accrueCashback(currency, amount)来累积返利。 - 修饰符包括
unlock,onlyEOA,notOnCashback。unlock修饰符利用TransientSlot临时存储UNLOCKED_TRANSIENT变量,使其在函数执行期间为true,以满足onlyUnlocked的要求。
- 这是用户发起支付的入口,它会调用
Nonce机制 (
consumeNonce):- 每次调用会使
nonce自增并返回新值。当nonce达到SUPERCASHBACK_NONCE时,NFT就会被铸造。
- 每次调用会使
最关键的门槛:
onlyDelegatedToCashback修饰符- 这个修饰符是本次挑战的核心。它检查
msg.sender.code,并通过内联汇编delegate := mload(add(code, 0x17))来读取msg.sender的字节码中的一个特定位置,期望那里存储的是CASHBACK_ACCOUNT的地址。 - 问题所在: EOA是没有代码的 (
msg.sender.code.length == 0)。当EOA通过EIP-7702进行“委托”时,它实际上是在其交易中包含了额外的字节码,使得msg.sender在接收合约看来,像一个临时部署的“智能账户”。如果我们可以控制这部分“临时部署”的字节码,我们就能伪造delegate的值。
- 这个修饰符是本次挑战的核心。它检查
揭秘后门:智能账户与字节码的华丽欺诈
挑战的线索提示“为高级用户预留了后门”,这通常意味着需要深入理解EIP-7702的底层实现,并利用其特殊性。
攻击思路核心:
问题的关键在于 onlyDelegatedToCashback 修饰符如何判断 msg.sender 是否已委托。它并不是检查 msg.sender 是否真的执行了 delegatecall,而是通过解析 msg.sender 的运行时字节码 (runtime code) 来查找一个特定的模式,即 CASHBACK_ACCOUNT 的地址被编码在字节码的 0x17 偏移量处。
我们不能直接修改 EOA 的字节码,但可以通过以下两种方式绕过:
部署伪造字节码的合约:
- 利用
MyFactory和deployFromBytecode,我们可以部署一个CashbackAttack合约。 - 在部署时,我们会将
CASHBACK_ACCOUNT的地址硬编码到CashbackAttack合约运行时字节码的0x17偏移量处。 - 这样,当
CashbackAttack合约调用Cashback合约的accrueCashback时,msg.sender(即CashbackAttack的地址) 会被onlyDelegatedToCashback修饰符错误地识别为已委托给CASHBACK_ACCOUNT。 - 同时,
CashbackAttack合约还会实现isUnlocked()(返回true) 和consumeNonce()。consumeNonce()被设计成第一次调用返回SUPERCASHBACK_NONCE(10000),之后返回0。
- 利用
利用
vm.signAndAttachDelegation模拟 EOA 智能账户:- Hardhat/Foundry 等测试框架提供了强大的
vm.signAndAttachDelegation函数。它允许我们为一个 EOA 附加一个临时的“委托”合约行为。这意味着当这个 EOA 发起交易时,它可以“携带”指定合约的逻辑。 - 更进一步,答案中的
MyNonce layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03语法非常关键。它指示MyNonce合约的存储布局应该与0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03这个地址的存储布局对齐。在 Foundry 中,结合vm.signAndAttachDelegation,我们可以让玩家的 EOA 地址在执行payWithCashback时,其nonce变量实际上操作的是一个部署在玩家 EOA 地址上的MyNonce实例的存储。 - 通过
MyNonce(payable(address(player))).setNonce(9999),我们可以直接设置玩家 EOA 的“智能账户”的nonce为 9999。 - 然后,当玩家 EOA 调用
Cashback的payWithCashback时,内部的CASHBACK_ACCOUNT.accrueCashback会调用Cashback(payable(msg.sender)).consumeNonce()。此时msg.sender就是玩家 EOA,但由于vm.signAndAttachDelegation和布局控制,它会调用MyNonce的consumeNonce,使其nonce变成 10000,从而铸造第二个 NFT。
- Hardhat/Foundry 等测试框架提供了强大的
双重打击,完美收割
具体的攻击步骤(根据 MyScript.sol):
- 准备阶段: 获取目标合约地址 (
CASHBACK_INST,NFT_INST,FREE_INST) 和玩家私钥。 - 第一次NFT攻击 (利用
CashbackAttack伪造智能账户):- 通过
MyFactory.deployFromBytecode部署CashbackAttack合约。这里的runtimeCodeTampered包含了CASHBACK_INST地址,伪装成已委托的智能账户。 CashbackAttack合约直接调用cashbackContract.accrueCashback两次 (一次原生币,一次ERC20代币),满足onlyDelegatedToCashback,onlyUnlocked,onlyOnCashback的所有条件。CashbackAttack的consumeNonce第一次返回SUPERCASHBACK_NONCE(10000),成功铸造第一个超级返利NFT给CashbackAttack地址。- 将获得的积分和NFT转移给
recovery地址 (即玩家地址)。
- 通过
- 第二次NFT攻击 (利用
MyNonce劫持玩家EOA的nonce):- 部署一个
MyNonce合约实例。 - 关键: 使用
vm.signAndAttachDelegation(address(myNonce), playerpk)。这让玩家的 EOA 在后续交互中,能够执行MyNonce合约的逻辑,并且其状态变量(如nonce)会存储在 EOA 的地址空间中。 - 通过
MyNonce(payable(address(player))).setNonce(9999),玩家地址上的nonce被设置为 9999。 - 玩家 EOA 调用
Cashback(payable(address(player))).payWithCashback。 - 在
payWithCashback内部,CASHBACK_ACCOUNT.accrueCashback被调用,它会进一步调用Cashback(payable(msg.sender)).consumeNonce()。此时msg.sender是玩家 EOA,但由于之前的vm.signAndAttachDelegation和MyNonce的布局,实际执行的是玩家 EOA 地址上的MyNonce实例的consumeNonce()。 MyNonce的nonce从 9999 变为 10000,触发第二个超级返利NFT的铸造,直接归属于玩家地址。
- 部署一个
- 积分最大化: 在上述攻击中,已经分别刷取了NATIVE_CURRENCY和FreedomCoin的最高返利额度。
胜利的号角:成为忠诚度计划的噩梦
我们成功了!通过对EIP-7702底层机制的深入理解和对Solidity字节码的巧妙利用,我们绕过了层层修饰符的限制,伪装智能账户身份,不仅刷满了所有支持货币的返利,更重要的是,我们拿到了两个超级返利NFT——一个归属于我们控制的地址(后转移到玩家),另一个直接铸造给了玩家地址本身。
这个挑战完美地展示了EIP-7702这类账户抽象技术在带来灵活性的同时,也可能引入新的攻击面。对 msg.sender.code 的盲目信任,以及对底层字节码和EIP-7702委托机制的不完全理解,都可能成为黑客利用的“后门”。
Cashback,你以为抓住了用户的忠诚,却不料,我们才是真正的“高级用户”,让你的返利计划,变成了我们的“提款机”! Gianfranco,干得漂亮!