ESPILON-CTF-2026-Writeups/IoT/Lain_VS_Knights/README.md

5.9 KiB
Executable File

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