| .. | ||
| README.md | ||
LAIN vs Knights — Solution
Difficulty: Hard | Category: IoT | Flag: ESPILON{0nlY_L41N_C4N_S0lv3}
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.
- TX (port 1111): Read only
- RX (port 2222): Write only
Step 1 — Enumerate nodes
bus.map
Returns: 1200 nodes total — 7 knights active.
Scan all nodes to find the 7 Knights and the Founder:
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
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)
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))
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
Step 2 — Get hints from Lain nodes
Connect to any Lain node and use node.truth repeatedly until you get:
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.
Step 3 — Defeat each Knight
Knight 1 — I2C_MIRROR
Find two distinct messages with the same byte sum mod N.
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()
node.i2c_write "0000"
node.i2c_write "7600"
node.submit_pair "0000" "7600"
# → fragment i2c_mirror=0000_7600
Knight 2 — CAN_CHECKSUM
Find a CAN frame whose CRC8 (poly=0x2F) equals the target byte.
def can_crc8(data, poly=0x2F):
c = 0
for b in data:
c ^= b
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
node.can_send "0026"
# → fragment can_checksum=0026
Knight 3 — SPI_PARITY
Find a byte with exactly 5 bit transitions (0↔1 between adjacent bits).
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'
node.spi_write 15
# → fragment spi_parity=15
Knight 4 — SRAM_WRITE
Write a specific value to a specific address.
node.write 0x8b 0x89
# → 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:
node.and_probe 0xff 0xff
# → "ff & ff = ff" (not our target — reveals target is 0xa8)
node.submit_and 0xa8 0xff
# → fragment logic_and=a8_ff
Knight 6 — FUSE_BITS
Probe with full mask to reveal the secret fuse bits directly:
node.fuse_probe 0xff
# → "Probe: (fuse & ff) = 30"
node.submit_fuse 0x30
# → fragment fuse_bits=30
Knight 7 — FAULT_INJECTION
Try all offset/mask combinations until the knight is purged:
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
node.inject 2 0x08
# → fragment fault_injection=2_08
Step 4 — Check fragment collection
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
Step 5 — Build the exploit
Concatenate fragments in the order Lain specified, hash with SHA-256, take first 24 hex chars:
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",
}
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"
Step 6 — Submit to the Founder and get the flag
bus.connect @node:0311
node.exploit 69b4a17e33b0cdace34b7610
# → "[ROOT] Exploit accepted! You are now root."
node.flag
# → ESPILON{0nlY_L41N_C4N_S0lv3}
Flag
ESPILON{0nlY_L41N_C4N_S0lv3}
Author
Eun0us