write-up: Intro/The_Wired/README.md

This commit is contained in:
Eun0us 2026-03-26 17:33:28 +00:00
parent 8bb0532b22
commit fc6080ae90

View File

@ -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 <HOST> 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 = "<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 = ["<hex_token>"]
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 = "<token>"
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 <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}`