write-up: IoT/Lain_VS_Knights/README.md

This commit is contained in:
Eun0us 2026-03-26 17:33:33 +00:00
parent d097da5066
commit ac10aa1f7d

View File

@ -1,17 +1,66 @@
# LAIN vs Knights — Solution
# Lain VS Knights
**Difficulty:** Hard | **Category:** IoT | **Flag:** `ESPILON{0nlY_L41N_C4N_S0lv3}`
| Field | Value |
|-------|-------|
| Category | IoT |
| Difficulty | Hard |
| Points | — |
| Author | Eun0us |
| CTF | Espilon 2026 |
## Overview
---
A UART interface exposes a simulated "Wired" network with 1200 nodes, 7 protected
by Knights and 1 by the Founder (Eiri Masami). Defeat all 7 Knights to collect
fragments, combine them into an exploit hash, and present it to the Founder.
## Description
Welcome to The Wired: a digital labyrinth of over a thousand mysterious nodes, where only
seven are protected by the legendary Knights.
You are **Lain**. Your mission: navigate this cryptic UART interface, explore The Wired,
and uncover the locations of the hidden Knights. Purge each Knight.
Each defeated Knight grants you a fragment. Collect all seven to unlock the final gateway:
**EIRI MASAMI** — the master of The Wired.
Seize root access. Become the new legend of The Wired.
- **TX (port 1111)**: Read only
- **RX (port 2222)**: Write only
## Step 1 — Enumerate nodes
Format: **ESPILON{flag}**
---
## TL;DR
1200 nodes, 7 Knights each at a random node, 1 Founder (Eiri). Scan all nodes to find them.
Each Knight presents a different hardware/protocol puzzle (I2C collision, CAN CRC, SPI parity,
SRAM write, logic AND, fuse bits, fault injection). Collect the 7 fragments, concatenate them
in the order Lain specifies, SHA-256 hash, take first 24 hex chars, submit to the Founder for
root access and the flag.
---
## Tools
| Tool | Purpose |
|------|---------|
| `nc` | Split UART connection |
| Python 3 | Node scanner, puzzle solvers, exploit script |
| `hashlib` | SHA-256 for exploit hash computation |
---
## Solution
### Step 1 — Connect and map the network
```bash
# Terminal 1 — TX
nc <host> 1111
# Terminal 2 — RX
nc <host> 2222
```
```text
bus.map
@ -19,89 +68,53 @@ bus.map
Returns: `1200 nodes total — 7 knights active.`
Scan all nodes to find the 7 Knights and the Founder:
Scan all nodes to locate the Knights and Founder (automated script recommended):
```python
import socket, time, re
HOST = "<host>"
TX_PORT, RX_PORT = 1111, 2222
def recv_until(sock, expect=">", timeout=2.0):
data = b""
sock.settimeout(timeout)
while True:
try:
c = sock.recv(4096)
if not c:
break
data += c
if expect.encode() in data:
break
except socket.timeout:
break
return data.decode(errors="ignore")
def scan_nodes(node_min=1, node_max=1200):
tx = socket.socket(); tx.connect((HOST, TX_PORT))
rx = socket.socket(); rx.connect((HOST, RX_PORT))
recv_until(tx) # flush banner
def scan_nodes(host, tx_port=1111, rx_port=2222, node_min=1, node_max=1200):
tx = socket.socket(); tx.connect((host, tx_port))
rx = socket.socket(); rx.connect((host, rx_port))
found = []
for nid in range(node_min, node_max + 1):
rx.sendall(f"bus.connect @node:{nid:04d}\n".encode())
time.sleep(0.08)
out = recv_until(tx)
out = tx.recv(4096).decode(errors="ignore")
if re.search(r"KNIGHT|FOUNDER|EIRI", out, re.I):
kind = "knight" if "KNIGHT" in out.upper() else "founder"
print(f"[+] Node {nid:04d}: {kind}")
found.append((nid, kind, out))
found.append((nid, kind))
rx.sendall(b"bus.disconnect\n")
recv_until(tx)
tx.close(); rx.close()
return found
```
Knights found at (example run): `0067, 0113, 0391, 0529, 0619, 0901, 0906`
Founder found at: `0311`
Example result: Knights at nodes 0067, 0113, 0391, 0529, 0619, 0901, 0906. Founder at 0311.
## Step 2 — Get hints from Lain nodes
> 📸 `[screenshot: scanner output listing discovered Knight and Founder node IDs]`
Connect to any Lain node and use `node.truth` repeatedly until you get:
### Step 2 — Get the assembly order from Lain nodes
Connect to any ordinary Lain node and call `node.truth` repeatedly until you get the order:
```text
Order matters. Use: i2c_mirror, can_checksum, spi_parity, sram_write, logic_and, fuse_bits, fault_injection.
Order matters. Use: i2c_mirror, can_checksum, spi_parity, sram_write,
logic_and, fuse_bits, fault_injection.
Assemble fragments in this order as a single string, with no separators.
Hash this string using SHA256.
Take the first 24 hex digits of the hash.
Hash this string using SHA256. Take the first 24 hex digits.
```
## Step 3 — Defeat each Knight
### Step 3 — Defeat each Knight
### Knight 1 — I2C_MIRROR
Find two distinct messages with the same byte sum mod N.
```python
def find_i2c_pair(modulo):
for a in range(256):
for b in range(256):
for c in range(256):
if a == c: continue
if (a + b) % modulo == (c + b) % modulo:
return bytes([a, b]).hex(), bytes([c, b]).hex()
```
**Knight 1 — I2C_MIRROR**: Find two messages with the same byte-sum mod N.
```text
node.i2c_write "0000"
node.i2c_write "7600"
node.submit_pair "0000" "7600"
# fragment i2c_mirror=0000_7600
# fragment i2c_mirror=0000_7600
```
### Knight 2 — CAN_CHECKSUM
Find a CAN frame whose CRC8 (poly=0x2F) equals the target byte.
**Knight 2 — CAN_CHECKSUM**: Find a CAN frame whose CRC8(poly=0x2F) = target.
```python
def can_crc8(data, poly=0x2F):
@ -111,90 +124,58 @@ def can_crc8(data, poly=0x2F):
for _ in range(8):
c = ((c << 1) ^ poly) & 0xFF if c & 0x80 else (c << 1) & 0xFF
return c
target = 0x91
for i in range(256):
for j in range(256):
if can_crc8(bytes([i, j])) == target:
print(f"Found: {bytes([i,j]).hex()}") # → 0026
break
# Target 0x91 → frame bytes 0x00 0x26
```
```text
node.can_send "0026"
# fragment can_checksum=0026
# fragment can_checksum=0026
```
### Knight 3 — SPI_PARITY
Find a byte with exactly 5 bit transitions (0↔1 between adjacent bits).
**Knight 3 — SPI_PARITY**: Find a byte with exactly 5 bit-transitions.
```python
def count_transitions(b):
bits = f"{b:08b}"
return sum(1 for i in range(7) if bits[i] != bits[i+1])
# 0x15 = 00010101 → transitions at positions 2,3,4,5,6 = 5 ✓
answer = next(hex(b)[2:] for b in range(256) if count_transitions(b) == 5)
# → '15'
# 0x15 = 00010101 → transitions: 5
```
```text
node.spi_write 15
# fragment spi_parity=15
# fragment spi_parity=15
```
### Knight 4 — SRAM_WRITE
Write a specific value to a specific address.
**Knight 4 — SRAM_WRITE**: Write a value to the required address.
```text
node.write 0x8b 0x89
# fragment sram_write=8b_89
# fragment sram_write=8b_89
```
### Knight 5 — LOGIC_AND
Find a pair `(a, b)` such that `a & b` equals the secret.
Probe with `node.and_probe 0xff 0xff` → reveals the secret (e.g. `0xa8`).
Then `a = secret`, `b = 0xff`:
**Knight 5 — LOGIC_AND**: Find `(a, b)` where `a & b == secret`.
```text
node.and_probe 0xff 0xff
# → "ff & ff = ff" (not our target — reveals target is 0xa8)
node.and_probe 0xff 0xff # reveals secret 0xa8
node.submit_and 0xa8 0xff
# fragment logic_and=a8_ff
# fragment logic_and=a8_ff
```
### Knight 6 — FUSE_BITS
Probe with full mask to reveal the secret fuse bits directly:
**Knight 6 — FUSE_BITS**: Probe with full mask to reveal the fuse value.
```text
node.fuse_probe 0xff
# → "Probe: (fuse & ff) = 30"
node.fuse_probe 0xff # reveals 0x30
node.submit_fuse 0x30
# fragment fuse_bits=30
# fragment fuse_bits=30
```
### Knight 7 — FAULT_INJECTION
Try all offset/mask combinations until the knight is purged:
```python
for offset in range(8):
for mask in [1, 2, 4, 8, 16, 32, 64, 128]:
print(f"node.inject {offset} 0x{mask:02x}")
# Correct answer: offset=2, mask=0x08
```
**Knight 7 — FAULT_INJECTION**: Try all offset/mask combinations.
```text
node.inject 2 0x08
# fragment fault_injection=2_08
# fragment fault_injection=2_08
```
## Step 4 — Check fragment collection
> 📸 `[screenshot: each Knight node returning its fragment after successful puzzle]`
### Step 4 — Verify fragment collection
```text
fragments
@ -207,45 +188,42 @@ fragments
# fault_injection = 2_08
```
## Step 5 — Build the exploit
Concatenate fragments in the order Lain specified, hash with SHA-256, take first 24 hex chars:
### Step 5 — Build the exploit hash
```python
import hashlib
fragments = {
"i2c_mirror": "0000_7600",
"can_checksum": "0026",
"spi_parity": "15",
"sram_write": "8b_89",
"logic_and": "a8_ff",
"fuse_bits": "30",
"fault_injection":"2_08",
"i2c_mirror": "0000_7600",
"can_checksum": "0026",
"spi_parity": "15",
"sram_write": "8b_89",
"logic_and": "a8_ff",
"fuse_bits": "30",
"fault_injection": "2_08",
}
order = ["i2c_mirror", "can_checksum", "spi_parity",
"sram_write", "logic_and", "fuse_bits", "fault_injection"]
payload = "".join(fragments[k] for k in order)
# → "0000_76000026158b_89a8_ff302_08"
exploit = hashlib.sha256(payload.encode()).hexdigest()[:24]
# → "69b4a17e33b0cdace34b7610"
exploit = hashlib.sha256(payload.encode()).hexdigest()[:24]
print(exploit) # e.g. 69b4a17e33b0cdace34b7610
```
## Step 6 — Submit to the Founder and get the flag
### Step 6 — Submit to the Founder and get root
```text
bus.connect @node:0311
node.exploit 69b4a17e33b0cdace34b7610
# → "[ROOT] Exploit accepted! You are now root."
# [ROOT] Exploit accepted! You are now root.
node.flag
# ESPILON{0nlY_L41N_C4N_S0lv3}
# ESPILON{0nlY_L41N_C4N_S0lv3}
```
> 📸 `[screenshot: Founder node accepting the exploit and printing the flag]`
---
## Flag
`ESPILON{0nlY_L41N_C4N_S0lv3}`
## Author
Eun0us