Skip to content
On this page

CTF 挑战:Cashback——揭秘链上银行的“双重返利”潘多拉魔盒

在加密世界的浩瀚星海中,DeFi创新层出不穷,而“Cashback”这家号称“城里最火爆的加密新银行”无疑是最耀眼的新星之一。它以诱人的承诺吸引着无数用户:每次链上支付都能赚取积分,积分累计到一定程度,便可晋升“传奇”地位,解锁梦寐以求的“超级返利NFT”徽章。

这听起来像是一个完美的数字黄金时代。但作为CTF挑战者,我们的任务并非安享返利,而是要成为这个忠诚度计划的“噩梦”:发掘隐藏的后门,在所有支持的货币中刷满返利,并最终攫取至少两个超级返利NFT,其中一个必须归属于我们自己的玩家地址。

一场围绕EIP-7702和智能合约漏洞的狩猎,就此拉开序幕!

Cashback的诱惑与陷阱:EIP-7702下的智能账户逻辑

Cashback的核心机制在于其巧妙利用EIP-7702标准,允许EOA(外部账户)像智能合约一样拥有更强大的功能,实现所谓的“智能账户”。用户需要将他们的EOA“委托”给Cashback合约,才能使用 payWithCashback 函数进行支付并累积返利。

合约设计解析:

  1. 积分累积 (accrueCashback):

    • 这个函数负责根据支付金额和货币类型计算返利,并铸造对应的ERC1155代币(代表积分)。
    • 关键在于,当 consumeNonce() 返回 SUPERCASHBACK_NONCE (10000) 时,会触发一个超级返利NFT的铸造。
    • 它受到严格的修饰符限制:onlyDelegatedToCashbackonlyUnlockedonlyOnCashback
  2. 支付入口 (payWithCashback):

    • 这是用户发起支付的入口,它会调用 currency.transfer 进行实际转账,然后内部调用 CASHBACK_ACCOUNT.accrueCashback(currency, amount) 来累积返利。
    • 修饰符包括 unlockonlyEOAnotOnCashbackunlock 修饰符利用 TransientSlot 临时存储 UNLOCKED_TRANSIENT 变量,使其在函数执行期间为 true,以满足 onlyUnlocked 的要求。
  3. Nonce机制 (consumeNonce):

    • 每次调用会使 nonce 自增并返回新值。当 nonce 达到 SUPERCASHBACK_NONCE 时,NFT就会被铸造。
  4. 最关键的门槛: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 的字节码,但可以通过以下两种方式绕过:

  1. 部署伪造字节码的合约:

    • 利用 MyFactorydeployFromBytecode,我们可以部署一个 CashbackAttack 合约。
    • 在部署时,我们会将 CASHBACK_ACCOUNT 的地址硬编码到 CashbackAttack 合约运行时字节码的 0x17 偏移量处。
    • 这样,当 CashbackAttack 合约调用 Cashback 合约的 accrueCashback 时,msg.sender (即 CashbackAttack 的地址) 会被 onlyDelegatedToCashback 修饰符错误地识别为已委托给 CASHBACK_ACCOUNT
    • 同时,CashbackAttack 合约还会实现 isUnlocked() (返回 true) 和 consumeNonce()consumeNonce() 被设计成第一次调用返回 SUPERCASHBACK_NONCE (10000),之后返回0。
  2. 利用 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 调用 CashbackpayWithCashback 时,内部的 CASHBACK_ACCOUNT.accrueCashback 会调用 Cashback(payable(msg.sender)).consumeNonce()。此时 msg.sender 就是玩家 EOA,但由于 vm.signAndAttachDelegation 和布局控制,它会调用 MyNonceconsumeNonce,使其 nonce 变成 10000,从而铸造第二个 NFT。

双重打击,完美收割

具体的攻击步骤(根据 MyScript.sol):

  1. 准备阶段: 获取目标合约地址 (CASHBACK_INST, NFT_INST, FREE_INST) 和玩家私钥。
  2. 第一次NFT攻击 (利用 CashbackAttack 伪造智能账户):
    • 通过 MyFactory.deployFromBytecode 部署 CashbackAttack 合约。这里的 runtimeCodeTampered 包含了 CASHBACK_INST 地址,伪装成已委托的智能账户。
    • CashbackAttack 合约直接调用 cashbackContract.accrueCashback 两次 (一次原生币,一次ERC20代币),满足 onlyDelegatedToCashbackonlyUnlockedonlyOnCashback 的所有条件。
    • CashbackAttackconsumeNonce 第一次返回 SUPERCASHBACK_NONCE (10000),成功铸造第一个超级返利NFT给 CashbackAttack 地址。
    • 将获得的积分和NFT转移给 recovery 地址 (即玩家地址)。
  3. 第二次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.signAndAttachDelegationMyNonce 的布局,实际执行的是玩家 EOA 地址上的 MyNonce 实例的 consumeNonce()
    • MyNoncenonce 从 9999 变为 10000,触发第二个超级返利NFT的铸造,直接归属于玩家地址。
  4. 积分最大化: 在上述攻击中,已经分别刷取了NATIVE_CURRENCY和FreedomCoin的最高返利额度。

胜利的号角:成为忠诚度计划的噩梦

我们成功了!通过对EIP-7702底层机制的深入理解和对Solidity字节码的巧妙利用,我们绕过了层层修饰符的限制,伪装智能账户身份,不仅刷满了所有支持货币的返利,更重要的是,我们拿到了两个超级返利NFT——一个归属于我们控制的地址(后转移到玩家),另一个直接铸造给了玩家地址本身。

这个挑战完美地展示了EIP-7702这类账户抽象技术在带来灵活性的同时,也可能引入新的攻击面。对 msg.sender.code 的盲目信任,以及对底层字节码和EIP-7702委托机制的不完全理解,都可能成为黑客利用的“后门”。

Cashback,你以为抓住了用户的忠诚,却不料,我们才是真正的“高级用户”,让你的返利计划,变成了我们的“提款机”! Gianfranco,干得漂亮!

Built with AiAda