Operator Interface¶
Authorized Use Only
Espilon is a security research and educational tool. Use it only on devices and networks you own or are explicitly authorized in writing to test.
C3PO is a Qt6-native desktop operator tool (PySide6 + qasync) for managing a fleet of ESP32 agents. Its window is split into a persistent sidebar, a stacked area holding six views, and an always-visible console at the bottom. This page documents each view in detail. For installation and invocation see Installation; for the C2 protocol and crypto see Protocol & Crypto.
Window layout¶
┌─────────────┬──────────────────────────────────────────────────┐
│ Sidebar │ Current View │
│ ───────── │ ──────────────────────────────────────────────── │
│ [Fleet] │ (FleetView / DeviceView / ModulesView / ...) │
│ [Device] │ │
│ [Modules] │ │
│ [Data] │ │
│ [Keystore] │ │
│ [Provision]│ │
│ ├────────────────────────────────────────────────── │
│ ● :2626 │ Console (log + command bar) │
│ 3 agents │ │
└─────────────┴──────────────────────────────────────────────────┘
| Component | Source | Role |
|---|---|---|
| Sidebar | app/widgets/sidebar.py |
Navigation buttons, server status badge (:2626), live agent count, logo |
| View stack | app/mainwindow.py (QStackedWidget) |
The six views, switched via sidebar buttons |
| Console strip | app/widgets/log_pane.py, command_bar.py |
Global log pane, device selector, and command bar, visible on every view |
flowchart LR
SB[Sidebar nav] --> VS[View stack]
VS --> FL[Fleet]
VS --> DV[Device]
VS --> MD[Modules]
VS --> DA[Data]
VS --> KS[Keystore]
VS --> PR[Provision]
C2[C2Events signals] --> VS
C2 --> CON[Console strip]
All views receive their data through C2Events Qt signals. The C2 layer never imports UI code, so the interface updates reactively as devices connect, respond, or drop.
1. Fleet view¶
File: app/views/fleet.py.
The Fleet view is the landing screen. It renders every connected device as a card in a responsive grid. Each card shows:
- Device ID (for example
esp32-home-01) - Status dot: green = Connected, yellow = Idle, red = Offline
- Chip type (for example
esp32c6) - IP address and port (for example
192.168.1.100:2626) - The loaded modules (for example
mod_network, mod_ble)
Click any card to open the Device view for that agent. When nothing is connected, the grid shows a waiting state ("No devices connected").
Live updates
Cards appear and disappear automatically as devices complete the handshake or disconnect. No manual refresh is needed.
2. Device view¶
File: app/views/device.py.
This is the main operational screen for a single agent.
┌──────────────┬────────────────────────────────────────────┐
│ Modules │ Log │
│ ──────────── │ [ device: esp32-home-01 ] │
│ mod_network │ > status │
│ mod_ble │ chip: esp32c6 heap: 234 KB uptime: 4m12s│
│ │ modules: mod_network, mod_ble │
│ [+] │ │
│ │ > ping 8.8.8.8 │
│ │ PING 8.8.8.8: 42 ms │
│ ├────────────────────────────────────────────┤
│ │ esp32-home-01 > _ Send │
└──────────────┴────────────────────────────────────────────┘
| Area | Behavior |
|---|---|
| Modules panel (left) | Modules currently loaded on this device. The + button jumps to the Modules view to inject more. |
| Log pane (centre) | All command output for this device, color-coded by message type (success, error, info). |
| Command bar (bottom) | Sends commands directly to this device. Up/Down arrows navigate command history. |
Commands use the form command_name [arg1 arg2 ...]. Output is correlated to the request through the protobuf request_id so responses land in the correct pane.
3. Modules view¶
File: app/views/modules.py.
A catalog browser plus the injection workflow. The catalog is built by ModuleCatalog from every module directory under the --modules path; each directory holds a module.toml manifest and its C source.
- Left list: all discovered modules.
- Right panel: the selected module's name, version, and description; its supported chips (
targets); the full command list with descriptions; and its required syscalls andmin_heap.
Example manifest the right panel reads:
[module]
name = "mod_network"
version = "1.2.0"
description = "Network toolkit: ping, ARP scan, TCP flood, tunnel"
targets = ["esp32", "esp32c6", "esp32s3", "esp32c3", "esp32s2"]
source = "cmd_network.c"
[commands]
ping = "ICMP echo to target host"
arp_scan = "ARP sweep on subnet"
dos_tcp = "TCP SYN flood <target> <port> <count>"
[requires]
syscalls = ["socket", "dns_resolve", "wifi_scan", "tcp_connect"]
min_heap = 32768
Inject flow¶
Pick a target device from the dropdown and press Inject. C3PO then:
sequenceDiagram
participant UI as Modules view
participant CC as compile_module
participant SG as sign_module
participant INJ as ModuleInjector
participant DEV as ESP32 agent
UI->>CC: compile .c -> ELF (asyncio.to_thread)
CC-->>UI: relocatable ELF bytes
UI->>SG: HMAC-SHA256 with master_key
SG-->>UI: [32B HMAC][ELF]
UI->>INJ: inject(blob, name)
INJ->>DEV: mod_chunk (1 KB chunks)
INJ->>DEV: mod_load name core timeout [persist]
DEV-->>INJ: load result (up to 30 s)
- Compile the C source to a relocatable ELF. Compilation is blocking, so it runs in
asyncio.to_thread. - Sign the ELF with the device's
master_key(HMAC-SHA256 via HKDF) producing a[32B HMAC][ELF]blob. - Transfer the blob in 1 KB chunks via
mod_chunk, then issuemod_load. - Show progress in the console; wait up to 30 seconds for the load to complete.
The toolchain is selected automatically from the chip: Xtensa GCC (xtensa-esp32-elf-gcc, xtensa-esp32s3-elf-gcc, ...) for ESP32 / S2 / S3, and riscv32-esp-elf-gcc for C3 / C6 / H2. Tools are located via PATH or ~/.espressif/tools/.
Where the tunnel lives
There is no dedicated tunnel view. Reverse tunnels (tun_start / tun_stop / tun_status in mod_network) are driven as ordinary commands from the Device view or the console. Camera streaming is planned for a future version.
4. Data view¶
File: app/views/data.py.
Aggregated intelligence tables, populated from the structured AGENT_DATA output of module commands. Each module emits pipe-delimited records that the Data view parses into rows. The view has three tabs.
| Tab | Populated by | Wire format | Columns |
|---|---|---|---|
| WiFi | mod_network scan, mod_fakeap |
AP\|SSID\|BSSID\|RSSI\|channel\|auth |
SSID, BSSID, RSSI, Ch, Auth, Device |
| BLE | mod_ble scan |
BLE\|addr\|name\|RSSI\|type |
Address, Name, RSSI, Type, Device |
| Network | mod_network arp_scan, mod_mitm |
HOST\|ip\|mac\|latency\|ports |
Host, MAC, Latency, Open Ports, Device |
Both tabs support:
- Sorting: click any column header.
- CSV export: the Export CSV button writes the current table to a file.
The trailing Device column records which agent produced each record, so results from a whole fleet aggregate into one table.
5. Keystore view¶
File: app/views/keystore.py.
Manage device keys without editing keys.json by hand. The table lists every registered device.
| Column | Notes |
|---|---|
| Device ID | Unique agent identifier |
| Key | Truncated hex preview (for example a1b2c3d4...4567); click to copy the full key |
| Transport | wifi or cellular |
| Board | Chip type (for example esp32c6) |
| Status | Live: Connected / Inactive / Offline |
Operations:
- Add: create an entry manually. Paste a hex key or press Generate to create a random 32-byte key.
- Edit: double-click a row (or the Edit button) to change key, transport, or board.
- Delete: remove an entry behind a confirmation dialog.
- Copy Key: place the full hex key on the clipboard.
- Reload: re-read
keys.jsonfrom disk to pick up external changes.
The keystore is the trust anchor
Each entry holds a device's 32-byte master_key. The handshake identifies a device only by trying each key in the store, so losing an entry makes that device unidentifiable until it is re-added. See Security.
Status values update live through Qt signals as devices connect and disconnect.
6. Provision view¶
File: app/views/provision.py.
Flash and register a new device from scratch. Beyond esptool (and optionally ESP-IDF for local builds) no external tools are needed.
Form fields¶
| Field | Required | Notes |
|---|---|---|
| Port | yes | Serial port (for example /dev/ttyACM0); auto-populated, with a Scan button to refresh |
| Chip | yes | Dropdown of supported variants |
| Device ID | no | Unique identifier; auto-generated as a random hex string if empty |
| Network | yes | Radio buttons: WiFi (SSID + password) or GPRS (APN, SIM PIN, board pins) |
| C2 Server | yes | IP and port (default 127.0.0.1:2626) |
| Firmware | yes | Path to a local Espilon-Firmware checkout, or empty to download the latest GitHub release |
| Keystore | yes | Path to keys.json (default keys.json) |
| Erase flash | no | Checkbox, recommended for first-time provisioning |
Supported chip variants in the dropdown:
CHIPS = ["esp32c6", "esp32s3", "esp32s3-v0.2", "esp32", "esp32c3", "esp32s2",
"esp32s3-cam-n16r8", "esp32-cam-aithinker"]
For GPRS, selecting a board profile auto-fills modem pins. Profiles include t-call and t-sim7070g. Camera board profiles (esp32s3-cam-n16r8, esp32-cam-aithinker) also exist, though runtime camera support is planned for a future version.
Provisioning sequence¶
flowchart TD
A[Generate master_key, 32 random bytes] --> B{Firmware?}
B -->|local checkout| C[idf.py build]
B -->|empty| D[Download GitHub release]
C --> E[Generate factory NVS, 0x14000, 24 KB]
D --> E
E --> F[Generate config NVS, 0x9000, 16 KB,<br/>ChaCha20-encrypted WiFi + C2 config]
F --> G[Flash all partitions via esptool]
G --> H[Save entry to keys.json]
H --> I[Device connects on next boot,<br/>appears in Fleet view]
- Generate the
master_key(32 random bytes) and store it in the keystore. - Build firmware via
idf.py build, or download release binaries. - Generate the factory NVS partition (
fctry, 0x14000, 24 KB) holding the plaintextmaster_keyand device ID. Flash encryption secures it at boot when enabled. - Generate the config NVS partition (
cfg, 0x9000, 16 KB) holding the ChaCha20-encrypted WiFi credentials and C2 address, using a key derived from the master key via HKDF. - Flash every partition with esptool: bootloader, partition table, NVS, OTA data, factory, firmware.
- Save the
device_idandmaster_keytokeys.json.
After provisioning, the device connects to C3PO on its next boot and appears in the Fleet view.
Sidebar and global console¶
The sidebar persists on every view. It holds the navigation buttons, the logo, a server status badge showing the listening port (for example ● :2626), and a live agent count.
The console strip is also always visible, so commands can be sent from any view without switching to Device.
Console esp32-home-01
─────────────────────────────────────────────
[+] esp32-home-01 192.168.1.100 (wifi)
> status
chip: esp32c6 heap: 234 KB uptime: 4m12s
─────────────────────────────────────────────
[ esp32-home-01 ▾ ] > _ Send
| Element | Behavior |
|---|---|
| Header | "Console" label plus the selected device ID in the accent color |
| Log pane | Scrolling terminal output: connection events ([+] device_id addr (transport)), system logs (blue/dim), command responses (white), errors (red) |
| Device selector | Choose the active device for console commands |
| Command bar | Send command_name [arg1 arg2 ...] to the selected device; Up/Down navigate history |
Auto-status and keep-alive probe replies are silent and do not appear in the console unless a command was user-initiated.
Related pages¶
- Protocol & Crypto - Management Protocol server, handshake, and crypto context
- Modules - writing and packaging modules
- Security - key handling and trust model
- Installation - dependencies and the
c3poentry point