#14 - Ethernaut Challenge 14 - Gatekeeper Two
Objective:
Make it past the gatekeeper and register as an entrant to pass this level.
Understanding the code:
// 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;
}
}
There is an address declared as the entrant, which we must assign ourselves to pass this level. There are three modifiers for three gates.
The first modifier, gateOne requires us to bypass the tx.origin error which is easy to do — we make a mediator contract(our attacking contract), and this check will pass.
The second modifier, gateTwo, is essentially inline assembly code to check that the extcodesize of the calling contract is 0. In simple terms, this modifier checks if the code's size at the caller's contract address is 0. We can bypass this by writing our code in the constructor itself since a contract does not have source code available during construction and will return extcodesize as 0.
The third modifier, gateThree, takes in a key as a parameter and has to satisfy its logic.
uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max)
This is the logic that states that
x ^ y = z
Note that the ^ symbol stands for XOR operation. And an important fact to know is that if x ^ y = z then x ^ z = y is also true. We will use this logic to get the key.
If you notice carefully in this equation, we can see that the y variable equivalent is uint64(_gateKey). This means that the key can be attained by simply rearranging the terms in this equation, which will give us the following:
uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max
This is the uint64(_gateKey) we require to enter the gates.
The enter function would grant us the challenge of getting accepted.
How to hack this contract?
- First of all, let’s check who the entrant is. Open the console and type await contract.entrant() to check the entrant’s address. Currently, it should be 0x00… Our goal is to make this into our address. Let’s copy and paste the Ethernaut Gatekeeper Two code onto Remix IDE.
- On Remix IDE, after copy-pasting this, we make the Hack contract like the following:
contract Hack{
constructor(address _target) public{
bytes8 _key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
GatekeeperOne(_target).enter(_key);
}
}
3. We pass the gateOne modifier by just making this code, and it ensures that the tx.origin and the msg.sender are different. The gateTwo modifier is bypassed since the extcodesize would be 0 when we are constructing the contract. The gateThree modifier requires the key. The logic of this key is defined in the previous section, and we take that key and insert it into this constructor.
4. Go ahead and deploy this contract with Ethernaut’s instance address for this challenge as the _target address. Once deployed, sign on Metamask and go back to the Ethernaut console, and type await contract.entrant(). You will notice that the entrant is now your address. Submit your instance, and you’re good to go. Congratulations!
Congratulations on completing this level, more solutions to the remaining challenges will be coming up in my next blog posts, so make sure to follow and clap for more similar content!
Thanks for reading this far. I wish you all the best!