Skip to content

Quick Start

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.

This guide takes you end to end: install the C3PO operator tool, launch it, provision and flash your first ESP32, watch it appear in the Fleet, run a built-in command, and inject your first module. Plan for about 15 minutes.

C3PO is a Qt6 desktop application (PySide6 + qasync). It bundles the Management Protocol server, the firmware provisioner, and the module catalog in a single window with six views: Fleet, Device, Modules, Data, Keystore, and Provision.


Prerequisites

  • An ESP32 board (any supported variant - see Hardware)
  • A USB cable with data lines (not charge-only)
  • Linux, macOS, or Windows (WSL2) host
  • Python 3.11+ and git
  • ESP-IDF v5.3.2 if you plan to build firmware locally or compile modules

What ESP-IDF gives you

ESP-IDF provides the cross-compilers used to (1) build firmware when you point the Provision view at a local checkout, and (2) compile modules on demand before injection. If you provision from a downloaded GitHub release and only inject pre-built modules, the IDF toolchains are still needed for module compilation. See Installation for the full ESP-IDF setup.


Step 1 - Install C3PO

Clone the framework and install C3PO in editable mode.

git clone --recursive https://github.com/Espilon-Net/Espilon-Firmware.git
# C3PO lives in the framework checkout
cd framework/c3po
pip install -e .

This installs the runtime dependencies and the c3po console entry point:

Dependency Purpose
Python 3.11+ Runtime (native asyncio + tomllib)
PySide6 Qt6 desktop UI
qasync Bridges the Qt event loop with asyncio
pycryptodome ChaCha20-Poly1305, HKDF, HMAC-SHA256
protobuf Command and AgentMessage serialization
esptool ESP32 flashing and chip detection

Optional extras

For RISC-V module compilation (C3, C6, H2) install the toolchain with sudo apt install gcc-riscv32-esp-elf. For serial-port auto-detection in the Provision view, pip install pyserial.


Step 2 - Launch C3PO

Start the application. C3PO opens its window and the embedded Management Protocol server begins listening on 0.0.0.0:2626.

c3po --keystore keys.json --modules modules/
Flag Default Description
--host 0.0.0.0 C2 bind address
--port 2626 C2 listen port
--keystore keys.json Device keystore (created on first provisioning)
--modules modules Directory scanned for module manifests
--debug off Verbose logging

The sidebar on the left switches between the six views. A status badge shows the server port (:2626) and the live agent count. A console strip is pinned at the bottom of every view: it carries a global log and a command bar so you can talk to a selected device without leaving the current view.

graph LR
    A[c3po launches] --> B[Management Protocol server binds 0.0.0.0:2626]
    A --> C[Qt window: 6 views]
    B --> D[Waits for device handshakes]
    C --> E[Provision view: flash a device]
    E --> D
    D --> F[Fleet view: device appears]

Step 3 - Provision a Device

Open the Provision view from the sidebar. Provisioning generates a per-device master key, writes encrypted configuration to the device, flashes firmware, and records the key in your keystore. No external tool is required beyond esptool (and ESP-IDF if you build locally).

Fill in the form:

  1. Port - the serial port (e.g. /dev/ttyACM0). Use Scan to refresh the list.
  2. Chip - select your target from the dropdown: esp32c6, esp32s3, esp32s3-v0.2, esp32, esp32c3, esp32s2, esp32s3-cam-n16r8, or esp32-cam-aithinker.
  3. Device ID - a unique name (e.g. esp32-home-01), or leave empty to auto-generate one.
  4. Network - choose WiFi (SSID + password) or GPRS (modem profile, APN, SIM PIN).
  5. C2 Server - the IP and port C3PO listens on (default 127.0.0.1:2626). For a real device on your LAN, use the host machine's LAN IP.
  6. Firmware - the path to a local Espilon-Firmware checkout to build from source, or leave empty to download the latest GitHub release.
  7. Keystore - the path to keys.json (matches your --keystore flag).
  8. Erase flash - recommended for a first-time provisioning.

Click to flash. The console reports progress through the provisioning sequence:

sequenceDiagram
    participant P as Provision view
    participant E as esptool
    participant D as ESP32
    P->>P: Generate 32-byte master_key
    P->>P: Build or download firmware
    P->>P: Generate factory NVS (master_key + device_id)
    P->>P: Generate config NVS (encrypted WiFi + C2 addr)
    P->>E: Flash bootloader, ptable, NVS, factory, app
    E->>D: write_flash via serial
    P->>P: Save device_id + master_key to keys.json

The master key is written to the device's factory (fctry) NVS partition at 0x14000 and the WiFi/server settings are ChaCha20-encrypted in the config NVS. The same master key is saved server-side in keys.json so the handshake can identify the device later.

Back up keys.json

keys.json is the only copy of your device master keys. If it is lost, the device can still boot but C3PO cannot identify it during the handshake, and it must be re-provisioned. Keep an encrypted backup after every provisioning run.

T-CALL pin note

If you provision a LilyGO T-CALL over GPRS, the hardware-correct modem pins are TX=17 / RX=16. See Hardware for the full board pin maps.


Step 4 - Watch It Appear in the Fleet

When the device boots, it reads its config, connects to your C2 address, and performs a silent three-step handshake. C3PO never sees the device ID in plaintext: it tries each key in keys.json, decrypts the device's frame, and identifies the agent.

Switch to the Fleet view. The device shows up as a card with:

  • Device ID (e.g. esp32-home-01)
  • A status dot (green = Connected, yellow = Idle, red = Offline)
  • Chip type (e.g. esp32c6)
  • IP address and port
  • Loaded modules (none yet on a fresh boot)

If nothing appears, confirm the Management Protocol server IP set during provisioning matches the host running C3PO, and that port 2626 is reachable from the device.


Step 5 - Run a Built-In Command

Click the device card to open the Device view. The right pane is a per-device log; the bottom is a command bar with Up/Down history.

Built-in commands ship in the firmware itself and need no module. Try status:

esp32-home-01 > status
chip: esp32c6  heap: 234 KB  uptime: 4m12s
modules: (none)
Built-in command Description
status Free heap, uptime, chip info, loaded modules
reboot Restart the device
cmds List all registered commands (static + module)
mod_list List loaded modules and their commands
mod_stop <name> Unload a module, free IRAM, unregister its commands
mod_purge Stop and unload every module

You can also send a command from the console strip on any view: pick the device in the selector and type the command.


Step 6 - Inject Your First Module

The firmware is a hollow shell - it contains no offensive capability at rest. Features arrive as signed, relocatable ELF modules loaded into RAM at runtime, and they are gone on the next power cycle.

Open the Modules view. The left list shows every module discovered under your --modules/ directory. Pick one, for example mod_network (which provides ping, arp_scan, and more) or mod_recon (wifi_scan). The right panel shows the module's version, description, supported chips, and command list.

To inject:

  1. Select the target device from the dropdown.
  2. Click Inject.

C3PO compiles the module's C source for the device's chip, signs the ELF with that device's master key (HMAC-SHA256), then transfers it.

graph LR
    A[Inject clicked] --> B[Compile .c for chip<br/>asyncio.to_thread]
    B --> C[Sign ELF: HKDF + HMAC-SHA256]
    C --> D[mod_chunk: 1KB chunks]
    D --> E[mod_load on device]
    E --> F[Verify + relocate into IRAM/PSRAM + run]
    F --> G[Commands appear on device]

Once loaded, the module's commands register on the device. Run one from the Device view:

esp32-home-01 > ping 8.8.8.8
PING 8.8.8.8: 42 ms

mod_network arp_scan and mod_ble ble_scan emit structured, pipe-delimited results that populate the Data view tabs (WiFi, BLE, Network), where you can sort and export to CSV.

Module signing is per-device

Because the signature is HMAC-SHA256 keyed by the device master key, a compiled module is bound to one device. C3PO recompiles and re-signs automatically for each target you inject to.


First-Run Recap

graph TD
    A["pip install -e . (framework/c3po)"] --> B["c3po --keystore keys.json --modules modules/"]
    B --> C[Provision view: flash device]
    C --> D[Fleet view: device Connected]
    D --> E[Device view: run status]
    E --> F[Modules view: inject mod_network]
    F --> G[Device view: ping 8.8.8.8]

Troubleshooting

Device will not enter download mode for flashing

Hold BOOT, press and release RESET, then release BOOT. Re-run the flash from the Provision view.

Provisioning fails to build firmware

Local builds need ESP-IDF v5.3.2 activated in the shell that launched C3PO (. ~/esp/esp-idf/export.sh). Alternatively, leave the Firmware path empty to download a release binary instead.

Device connects but C3PO logs decrypt or handshake errors

The key in keys.json does not match the key flashed to the device. Re-provision the device, or add the correct 32-byte master key in the Keystore view.

Device never appears in Fleet
  • Confirm the Management Protocol server IP set in the Provision step matches the host running C3PO.
  • Check that port 2626 is reachable from the device (firewall, NAT).
  • For WiFi devices, confirm the SSID/password are correct and the network is 2.4 GHz.
  • For GPRS devices, the Management Protocol server must be a public IP - a 192.168.x.x address will not work.
Toolchain not found during module injection

Module compilation needs the ESP-IDF cross-compiler on PATH (or under ~/.espressif/tools/). Activate ESP-IDF in the terminal where you launched C3PO, then restart it.


Next Steps

  • Installation - full ESP-IDF setup, multi-target toolchains, and dependency details
  • Hardware Guide - supported boards, BLE/PSRAM/panic matrix, GPRS pinouts
  • ESPM Module Reference - write and inject your own modules (ESPM v0.2.0, 102 syscalls)