Avatar

Eldoria Gate


  
      
Malakar 1b:22-28, Tales from Eldoria - Eldoria Gates

In ages past, where Eldoria's glory shone,
Ancient gates stand, where shadows turn to dust.
Only the proven, with deeds and might,
May join Eldoria's hallowed, guiding light.
Through strict trials, and offerings made,
Eldoria's glory, is thus displayed.
      
  
               ELDORIA GATES      
         *_   _   _   _   _   _  *
 ^       | `_' `-' `_' `-' `_' `-|       ^ 
 |       |                       |       |
 |  (*)  |     .___________      |  \^/  |
 | _<#>_ |    //           \     | _(#)_ |
o+o \ / \0    ||   =====   ||    0/ \ / (=)
 0'\ ^ /\/    ||           ||    \/\ ^ /`0
   /_^_\ |    ||    ---    ||    | /_^_\
   || || |    ||           ||    | || ||
   d|_|b_T____||___________||____T_d|_|b
    

Challenge Files

We are given three solidity files: Setup.sol, EldoriaGate.sol, EldoriaGateKernel.sol.

Setup.sol

As the name implies, this Solidity file sets up the contracts for the challenge. Like most blockchain CTF setups, it also checks whether the instance is solved.

EldoriaGate.sol

The main logic is split into two files, one of them being EldoriaGate.sol. We see cool ASCII art and a description that speaks about the glory of Eldoria. But we must prove ourselves before we can become part of the kingdom. In our test, we see two guards protecting the gate that leads into the kingdom.

In the kingdom, villagers are assigned different roles. Each villager is described by a struct that has the following variables:

  • uint id: The unique identification number for each villager.
  • bool authenticated: Dictates whether or not the villager is allowed in Eldoria.
  • uint8 roles: The assignment of roles is based on a bitmask and is stored in this variable.

If a villager wants to enter Eldoria, they must know the secret that allows them to get past the guards at the gate. But only the worthy may obtain it (more on this later). The mapping of the villagers’ roles is as follows:

  0b00000001 -> SERF
  0b00000010 -> PEASANT 
  0b00000100 -> ARTISAN 
  0b00001000 -> MERCHANT 
  0b00010000 -> KNIGHT 
  0b00100000 -> BARON 
  0b01000000 -> EARL 
  0b10000000 -> DUKE

But here comes a good question: the setup will give us the flag if we are a usurper. However, there is no role in the mapping for that, so how do we become one?

The answer lies in the function called checkUsurper:

function checkUsurper(address _villager) external returns (bool) {
    (uint id, bool authenticated , uint8 rolesBitMask) = kernel.villagers(_villager);
    bool isUsurper = authenticated && (rolesBitMask == 0);
    emit UsurperDetected(
        _villager,
        id,
        "Intrusion to benefit from Eldoria, without society responsibilities, without suspicions, via gate breach."
    );
    return isUsurper;
}
 Villager has a usurper role if and only if roleBitMask is set to 0

EldoriaGateKernel.sol

Remember how only the worthy get to become authenticated? Lucky for us, Malakar’s corruption has weakened the kingdom’s defenses, leaving vulnerabilities in the gate’s security system. Actually, yeah… you just need to know a password that’s publicly available. Not much of a password, eh?

To achieve this, we can use Python’s Web3 library (which I urge the reader to try themselves) to retrieve the value of slot 0. Let’s examine the memory layout and confirm that slot 0 holds the passphrase.

╭---------------+-------------------------------------------------------+------+--------+-------╮
| Name          | Type                                                  | Slot | Offset | Bytes |
+===============================================================================================+
| eldoriaSecret | bytes4                                                | 0    | 0      | 4     |
|---------------+-------------------------------------------------------+------+--------+-------|
| villagers     | mapping(address => struct EldoriaGateKernel.Villager) | 1    | 0      | 32    |
|---------------+-------------------------------------------------------+------+--------+-------|
| frontend      | address                                               | 2    | 0      | 20    |
╰---------------+-------------------------------------------------------+------+--------+-------╯
 Villager is authenticated if and only if they know the passphrase (2)

Becoming the Power

Okay, now we know the passphrase to become authenticated, but how do we set the role bitmask to 0 so that we become a usurper?

The function responsible for determining the role of the villager is called evaluateIdentity.

function evaluateIdentity(address _unknown, uint8 _contribution) external onlyFrontend returns (uint id, uint8 roles) {
    assembly {
        mstore(0x00, _unknown)
        mstore(0x20, villagers.slot)
        let villagerSlot := keccak256(0x00, 0x40)

        mstore(0x00, _unknown)
        id := keccak256(0x00, 0x20)
        sstore(villagerSlot, id)

        let storedPacked := sload(add(villagerSlot, 1))
        let storedAuth := and(storedPacked, 0xff)
        if iszero(storedAuth) { revert(0, 0) }

        let defaultRolesMask := ROLE_SERF
        roles := add(defaultRolesMask, _contribution)
        if lt(roles, defaultRolesMask) { revert(0, 0) }

        let packed := or(storedAuth, shl(8, roles))
        sstore(add(villagerSlot, 1), packed)
    }
}

You can be thorough and go over the Solidity assembly code (which I also urge the reader to do), or you can be lazy like me and simply test what the function does when we pass different values to it.

One weird thing I noticed immediately was that the roles variable is a uint8. The good thing is that integer overflows and underflows are not checked in the assembly code.

Sooo… what happens if we call this function with _contribution set to 255 (which is the max value of uint8)?

The defaultRolesMask is ROLE_SERF, which by itself is 1 << 0, meaning it’s simply 1. This means the overflow will actually set roles to 0, thereby making us the usurper.

Code that Brings Power

We have become the usurper. Here is the final code that achieves this:

s = Setup(0x0); // Setup address goes here
eg = EldoriaGate(address(s.TARGET()));
bytes4 secret = 0xdeadfade;
eg.enter{value: 0xff}(secret);

But remember, he who seizes the crown by force can never wear it in peace.

All tags