diff --git a/Intro/The_Wired/README.md b/Intro/The_Wired/README.md index 730b201..eef1643 100644 --- a/Intro/The_Wired/README.md +++ b/Intro/The_Wired/README.md @@ -1,10 +1,64 @@ -# The Wired — Writeup +# The Wired -**Difficulty:** Medium | **Flag:** `ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}` +| Field | Value | +|-------|-------| +| Category | Intro | +| Difficulty | Medium | +| Points | 400 | +| Author | Eun0us | +| CTF | Espilon 2026 | -## Recon +--- -Connect to the Navi shell on port 1337 and start reading. +## Description + +Something was interrupted. + +The agents are still flashed. The link is broken. The devices continue to boot, identify +themselves, and wait for instructions they never receive. + +You have gained access to a machine that was once used to administer a fleet of ESP32-based +agents. Logs, firmware dumps, technical notes — everything is still there. + +The coordination point is still listening. + +If you can understand how the agents communicate, prove your identity, and complete the +handshake — the coordinator will tell you what it knows. + +**Ports:** +- 1337: Navi Shell (investigation) +- 2626: C2 Coordinator + +Format: **ESPILON{flag}** + +*"No matter where you go, everyone's connected."* + +--- + +## TL;DR + +Read the files on the investigation machine (port 1337) to understand a 2-step ChaCha20 + +Protobuf C2 protocol. Extract the real crypto key from firmware ELF strings (avoid the +honeypot dev key). Impersonate device `ce4f626b` (the "Eiri_Master" root coordinator). +Send an `AGENT_INFO` message, capture the session token from the response, then send a +`CMD_RESULT` message with that token — all on the same TCP connection — to receive the flag. + +--- + +## Tools + +| Tool | Purpose | +|------|---------| +| `nc` | Connect to Navi Shell | +| Python 3 + `pycryptodome` | ChaCha20 encrypt/decrypt | +| `strings` / Ghidra | Extract key from ELF binary | +| Manual Protobuf encoding | Serialize AgentMessage | + +--- + +## Solution + +### Step 1 — Explore the investigation machine ```bash nc 1337 @@ -12,30 +66,46 @@ cat README_FIRST.txt ls -la ``` -Directories: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/` +Directories present: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/` -The key files to understand the protocol: +**Critical files:** -- `notes/protocol.txt` — frame format is `base64( ChaCha20( protobuf(AgentMessage) ) ) + '\n'` -- `notes/derivation.txt` — ChaCha20 with 32-byte key, 12-byte nonce, counter=0 -- `notes/hardening.txt` — keys are baked into the firmware ELF. **WARNING:** there's a dev key at the bottom (`Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG`) — it's a trap planted by "the other Lain". The server drops you silently if you use it. The journal entry from Jan 17 warns about this. +- `notes/protocol.txt` — frame format: + ``` + base64( ChaCha20( protobuf(AgentMessage) ) ) + '\n' + ``` +- `notes/derivation.txt` — ChaCha20 key is 32 bytes, nonce is 12 bytes, counter=0 +- `notes/hardening.txt` — warns about a trap dev key at the bottom; Jan 17 journal entry + confirms the key `Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG` is planted bait +- `tools/devices.json` — lists all known device IDs with roles -## Identify the target +> 📸 `[screenshot: notes/protocol.txt showing the frame format]` -`notes/eiri.txt` and `tools/devices.json` tell you the interesting device is `ce4f626b` — alias "Eiri_Master", role `root-coordinator`, status `quarantine`. Regular devices just get a `heartbeat` back. This one triggers the flag path. +### Step 2 — Identify the target device -## Understand the handshake +Read `notes/eiri.txt` and `tools/devices.json`: -`comms/msg_ops_20260114.txt` describes the 2-step session: +``` +Device: ce4f626b +Alias: Eiri_Master +Role: root-coordinator +Status: quarantine +``` + +Regular devices receive a `heartbeat` response. Only `ce4f626b` triggers the flag path. + +> 📸 `[screenshot: devices.json showing the Eiri_Master entry with root-coordinator role]` + +### Step 3 — Understand the handshake + +From `comms/msg_ops_20260114.txt`: 1. Agent sends `AGENT_INFO` → coordinator replies `session_init` with a random token in `argv[0]` 2. Agent sends `AGENT_CMD_RESULT` with `request_id` = that token → coordinator replies with the flag -`comms/msg_lain_20260116.txt` confirms both messages have to go on the **same TCP connection**. Token is per-connection, can't reuse it. +Both messages must go over the **same TCP connection**. The token is per-connection. -## Extract the key - -Any device ELF works since they all share the same key (see `notes/changelog.txt` about v0.9.0 migration). +### Step 4 — Extract the ChaCha20 key ```bash strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{32}$' @@ -45,47 +115,68 @@ strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$' - Key (32 bytes): `7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG` - Nonce (12 bytes): `X3kW7nR9mPq2` -Alternative: Ghidra → find `chacha_cd()` in crypto.c → follow xrefs to `CONFIG_CRYPTO_KEY` / `CONFIG_CRYPTO_NONCE`. +Do **not** use the key found in `notes/hardening.txt` — it is a honeypot. -## Exploit +> 📸 `[screenshot: strings output showing the real 32-character key]` -Single TCP connection to port 2626. +### Step 5 — Send the two-message handshake -**MSG1 — AGENT_INFO:** -``` -AgentMessage { - device_id = "ce4f626b" - type = 0 (INFO) - payload = b"ce4f626b" -} +```python +import base64, socket +from Crypto.Cipher import ChaCha20 + +HOST = "" +PORT = 2626 +KEY = b"7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG" +NONCE = b"X3kW7nR9mPq2" + +def encrypt_frame(plaintext): + cipher = ChaCha20.new(key=KEY, nonce=NONCE) + return base64.b64encode(cipher.encrypt(plaintext)) + b"\n" + +def decrypt_frame(frame): + raw = base64.b64decode(frame.strip()) + cipher = ChaCha20.new(key=KEY, nonce=NONCE) + return cipher.decrypt(raw) + +# Manually encode AgentMessage as protobuf +# field 1 (device_id) = "ce4f626b", field 2 (type) = 0 (INFO), field 5 (payload) = b"ce4f626b" +def encode_agent_info(): + dev_id = b"ce4f626b" + msg = b"" + msg += b"\x0a" + bytes([len(dev_id)]) + dev_id # field 1 = device_id + msg += b"\x10\x00" # field 2 = type 0 (INFO) + msg += b"\x2a" + bytes([len(dev_id)]) + dev_id # field 5 = payload + return msg + +with socket.create_connection((HOST, PORT)) as s: + # MSG1 — AGENT_INFO + s.sendall(encrypt_frame(encode_agent_info())) + resp_raw = s.recv(4096) + resp_pb = decrypt_frame(resp_raw) + + # Extract token from argv[0] in the Command protobuf response + # (parse manually or use protobuf library) + token = extract_token(resp_pb) + + # MSG2 — CMD_RESULT (type=4) + def encode_cmd_result(token): + dev_id = b"ce4f626b" + msg = b"" + msg += b"\x0a" + bytes([len(dev_id)]) + dev_id + msg += b"\x10\x04" # type 4 = CMD_RESULT + msg += b"\x22" + bytes([len(token)]) + token.encode() # field 4 = request_id + msg += b"\x2a" + bytes([len(dev_id)]) + dev_id + return msg + + s.sendall(encrypt_frame(encode_cmd_result(token))) + flag_resp = s.recv(4096) + flag_pb = decrypt_frame(flag_resp) + print(flag_pb) ``` -Serialize as protobuf → ChaCha20 encrypt → base64 + `\n` → send. +The server replies: -Server replies with: -``` -Command { - command_name = "session_init" - argv = [""] - request_id = "handshake" -} -``` - -Decrypt + decode the response, grab the token from `argv[0]`. - -**MSG2 — CMD_RESULT:** -``` -AgentMessage { - device_id = "ce4f626b" - type = 4 (CMD_RESULT) - request_id = "" - payload = b"ce4f626b" -} -``` - -Same connection. Encrypt, encode, send. - -Server replies: ``` Command { command_name = "flag" @@ -93,43 +184,19 @@ Command { } ``` -## Things that will get you dropped +> 📸 `[screenshot: solver output showing the decrypted flag response]` -- Using the fake dev key from `hardening.txt` -- Sending a device_id not in the allowlist -- Using a valid but non-master device (you get `heartbeat`, not `flag`) -- Sending MSG2 on a new connection (token is tied to the session) -- Wrong `type` in MSG2 (needs to be `4`) -- Wrong `request_id` (needs to match the token exactly) +### Things that will get you silently dropped -## Solver +- Using the honeypot dev key from `hardening.txt` +- Sending a `device_id` not in the allowlist +- Using a valid but non-master device (returns `heartbeat`, not `flag`) +- Sending MSG2 on a **new** TCP connection (token is session-scoped) +- Wrong `type` in MSG2 (must be `4`) +- Wrong `request_id` (must match the token exactly) -```bash -python3 solve.py --host --port 2626 -``` +--- -## Protobuf reference +## Flag -The `.proto` is at `wired/registry/c2.proto` on the target: - -```protobuf -message AgentMessage { - string device_id = 1; - AgentMsgType type = 2; - string source = 3; - string request_id = 4; - bytes payload = 5; - bool eof = 6; -} - -message Command { - string device_id = 1; - string command_name = 2; - repeated string argv = 3; - string request_id = 4; -} -``` - -No need for `protoc` — manual varint encoding works fine. See `solve.py`. - -Author: Eun0us +`ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`