Ethernaut(Lvl 14): Empty Contracts ?
Contract's state at initialization and bitwise operations.
Step 1: Sending a Transaction to Create a Contract
Imagine you're sending a letter to a friend to ask them to create a new contract for you. This letter contains several pieces of information:
Sender (s): This is like your address. It tells the Ethereum network who is asking for the contract to be created.
Original Transactor (o): This is the address of the person who actually sent the letter. It might be different from the sender if you used a friend's address to send the letter.
Available Gas (g): This is like the budget you're giving your friend to create the contract. It's the total amount of "fuel" they can use to do the job.
Gas Price (p): This is the current rate for using gas. It's like the cost of gas per mile.
Endowment (v): This is the amount of money you're sending to your friend to help them create the contract. It's usually zero, but it can be more if you want to give them a head start.
Initialization EVM Code (i): This is the blueprint for the contract. It's like the instructions your friend needs to follow to create the contract.
Step 2: Calculating the Contract's Address
Before your friend starts creating the contract, they calculate where the contract will live on the Ethereum network. This is like deciding on a house number for your friend's new contract.
Step 3: Creating the Contract
Now, your friend starts building the contract using the blueprint you provided. They change the state variables, store data, and use gas as they go along.
Step 4: The Contract is Born
Once your friend finishes building the contract, they store the blueprint at the address they calculated earlier. This is like your friend moving into the house they built.
Step 5: Returning the Remaining Gas and a Success/Failure Message
After the contract is created, your friend sends you a message to let you know if they were successful and how much gas they used up.
Important Note:
During the contract creation process, if you try to check the size of the contract's code, you'll find it's empty. This is because the contract doesn't exist yet, so it can't have any code.
Bitwise Operations in Solidity
Solidity also supports some basic logic operations:
& (and): This is like asking if both conditions are true. For example, if you have 1010 and 1111, the result is 1010.
| (or): This is like asking if at least one condition is true. For example, if you have 1010 and 1111, the result is 1111.
^ (xor): This is like asking if only one condition is true. For example, if you have 1010 and 1111, the result is 0101.
~ (not): This is like asking if the condition is not true. For example, if you have 1010, the result is 0101.
Remember, in Solidity, you use **
for exponentiation, not ^
.
The Challenge
The challenge , similar to gatekeeper one requires us to register as an entrant to pass the level.
// 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;
}
}
Constructor Execution: When the attack contract is deployed, its constructor should be immediately executed. The constructor should take the address of the
GatekeeperTwo
contract as an argument.Gate Key Calculation:
The attack contract calculates a gate key (
gateKey
) using the address of the hacker contract (address(this)
).The
keccak256
hash function is applied to the address of the hacker contract, resulting in a 32-byte hash value.The first 8 bytes of this hash value are converted into a
bytes8
type and then cast to auint64
.This
uint64
value is bitwise XORed with the maximumuint64
value (uint64(0) - 1
). The purpose of this XOR operation is to flip all the bits of the hash value.
Interface Interaction:
The attack contract should interacts with the
GatekeeperTwo
contract via an interfaceIGatekeeperTwoInterface
.It calls the
enter
function of theGatekeeperTwo
contract, passing the crafted gate key (bytes8(gateKey)
).
GatekeeperTwo Vulnerability:
The vulnerability lies in the
GatekeeperTwo
contract's gatekeeping mechanism. It incorrectly trusts the gate key provided by external contracts without proper validation.Due to the specific crafting of the gate key in the
GatekeeperTwoHacker
contract, the gatekeeper mechanism ofGatekeeperTwo
fails to recognize it as unauthorized entry.
Attack Execution:
The attack contract should successfully bypass the gatekeeper mechanism of the
GatekeeperTwo
contract by providing the crafted gate key.As a result, the attacker gains unauthorized access to the protected functionality or resources controlled by the
GatekeeperTwo
contract.
```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
interface IGatekeeperTwoInterface { function enter(bytes8 _gateKey) external returns (bool); }
contract GatekeeperTwoHacker{
constructor(address _gatekeeperTwo){
uint64 gateKey;
unchecked{
gateKey = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1);
}
IGatekeeperTwoInterface victim = IGatekeeperTwoInterface(_gatekeeperTwo);
victim.enter(bytes8(gateKey));
}
}
//Had to add unchecked cause the compiler kept on
//throwing erros due to the overflowing of values to achieve
//desired result
```
Developer TidBits
Aside from contract blackholes, another possibility arises: Zombie contracts can be crafted by interrupting contract initialization. These resulting contracts possess an address but are forever void of any code, rendering them incapable of returning the initial purpose to you.